From 143c37d36f032e7ebcb1934500c24f8d7f25c1f9 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sat, 29 Jan 2022 02:06:56 -0600 Subject: [PATCH] cleaned up liquidation price methods --- freqtrade/exchange/binance.py | 50 ++++++------ freqtrade/exchange/exchange.py | 135 ++++++++------------------------ freqtrade/exchange/okex.py | 4 +- freqtrade/freqtradebot.py | 11 +-- tests/exchange/test_exchange.py | 70 +++++------------ 5 files changed, 79 insertions(+), 191 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 164e94060..981126232 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -277,18 +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_helper( + def liquidation_price( self, open_rate: float, # Entry price of position is_short: bool, - leverage: float, mm_ratio: float, position: float, # Absolute value of position size - trading_mode: TradingMode, - collateral: Collateral, - maintenance_amt: Optional[float] = None, # (Binance) - wallet_balance: Optional[float] = None, # (Binance and Gateio) + 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 ) -> Optional[float]: @@ -299,19 +296,15 @@ 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 leverage: The amount of leverage on the trade :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 trading_mode: SPOT, MARGIN, FUTURES, etc. - :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 - - # * Not required by Binance - :param taker_fee_rate: + :param taker_fee_rate: # * Not required by Binance + :param maintenance_amt: # * Only required for Cross :param mm_ex_1: (TMM) @@ -321,31 +314,32 @@ class Binance(Exchange): Cross-Margin Mode: Unrealized PNL of all other contracts, excluding Contract 1. Isolated-Margin Mode: 0 """ - if trading_mode == TradingMode.SPOT: + 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 collateral: + if (maintenance_amt is None): raise OperationalException( - "Parameter collateral is required by liquidation_price when trading_mode is " - f"{trading_mode}" + f"Parameter maintenance_amt is required by Binance.liquidation_price" + f"for {self.collateral.value} {self.trading_mode.value}" ) - 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" + + if (self.collateral == Collateral.CROSS and (mm_ex_1 is None or upnl_ex_1 is None)): raise OperationalException( - f"Parameters {required_params} are required by Binance.liquidation_price" - f"for {collateral.name} {trading_mode.name}" + 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 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 ( ( (wallet_balance + cross_vars + maintenance_amt) - @@ -356,4 +350,4 @@ class Binance(Exchange): ) 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") diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index c7d837471..f3b4feb40 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -2018,135 +2018,62 @@ class Exchange: self, open_rate: float, # Entry price of position 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) + 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 ) -> Optional[float]: """ + PERPETUAL: + gateio: https://www.gate.io/help/futures/perpetual/22160/calculation-of-liquidation-price + okex: https://www.okex.com/support/hc/en-us/articles/ + 360053909592-VI-Introduction-to-the-isolated-mode-of-Single-Multi-currency-Portfolio-margin + :param exchange_name: - :param open_rate: (EP1) Entry price of position + :param open_rate: 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 mm_ratio: :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) + :param wallet_balance: Amount of collateral in the wallet being used to trade 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 + # * Not required by Gateio or OKX + :param maintenance_amt: + :param mm_ex_1: + :param upnl_ex_1: """ - if trading_mode == TradingMode.SPOT: + 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 collateral: + if (not taker_fee_rate): raise OperationalException( - "Parameter collateral is required by liquidation_price when trading_mode is " - f"{trading_mode}" + f"Parameter taker_fee_rate is required by {self.name}.liquidation_price" ) - 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, - ) + 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") - def liquidation_price_helper( - self, - open_rate: float, # Entry price of position - is_short: bool, - leverage: float, - mm_ratio: float, - position: float, # Absolute value of position size - wallet_balance: float, # Or margin balance - trading_mode: TradingMode, - collateral: Collateral, - 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 - ) -> Optional[float]: - """ - PERPETUAL: - gateio: https://www.gate.io/help/futures/perpetual/22160/calculation-of-liquidation-price - okex: https://www.okex.com/support/hc/en-us/articles/ - 360053909592-VI-Introduction-to-the-isolated-mode-of-Single-Multi-currency-Portfolio-margin + value = wallet_balance / position - :param exchange_name: - :param open_rate: 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 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 trading_mode == TradingMode.SPOT: - return None - - if (not taker_fee_rate): - raise OperationalException( - f"Parameter taker_fee_rate is required by {self.name}.liquidation_price" - ) - - if trading_mode == TradingMode.FUTURES and collateral == Collateral.ISOLATED: - # if is_inverse: - # raise OperationalException( - # "Freqtrade does not support inverse contracts at the moment") - - value = wallet_balance / position - - mm_ratio_taker = (mm_ratio + taker_fee_rate) - if is_short: - return (open_rate + value) / (1 + mm_ratio_taker) - else: - return (open_rate - value) / (1 - mm_ratio_taker) + mm_ratio_taker = (mm_ratio + taker_fee_rate) + if is_short: + return (open_rate + value) / (1 + mm_ratio_taker) else: - raise OperationalException( - f"{self.name} does not support {collateral.value} {trading_mode.value}") + return (open_rate - value) / (1 - mm_ratio_taker) + else: + raise OperationalException( + 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: diff --git a/freqtrade/exchange/okex.py b/freqtrade/exchange/okex.py index a3623584c..b7babe6e9 100644 --- a/freqtrade/exchange/okex.py +++ b/freqtrade/exchange/okex.py @@ -1,8 +1,6 @@ import logging -from typing import Dict, List, Optional, Tuple - +from typing import Dict, List, Tuple from freqtrade.enums import Collateral, TradingMode -from freqtrade.exceptions import OperationalException from freqtrade.exchange import Exchange diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 0485e9cc3..f39c97b3a 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -626,16 +626,13 @@ class FreqtradeBot(LoggingMixin): isolated_liq = self.exchange.liquidation_price( open_rate=open_rate, is_short=is_short, - leverage=leverage, - trading_mode=self.trading_mode, - collateral=Collateral.ISOLATED, - mm_ex_1=0.0, - upnl_ex_1=0.0, + 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_ratio=mm_ratio, - taker_fee_rate=taker_fee_rate + mm_ex_1=0.0, + upnl_ex_1=0.0, ) else: isolated_liq = self.exchange.get_liquidation_price(pair) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 7ba9bb2ee..cfdcaa596 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -3626,6 +3626,8 @@ def test_get_liquidation_price(mocker, default_conf): exchange_has=MagicMock(return_value=True), ) default_conf['dry_run'] = False + default_conf['trading_mode'] = 'futures' + default_conf['collateral'] = 'isolated' exchange = get_patched_exchange(mocker, default_conf, api_mock) 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', [ # 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, @@ -3991,51 +3993,22 @@ def test_liquidation_price_is_none( trading_mode, collateral ): + default_conf['trading_mode'] = trading_mode + default_conf['collateral'] = collateral exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) assert exchange.liquidation_price( - open_rate, - is_short, - leverage, - trading_mode, - collateral, - 1535443.01, - 71200.81144, - -56354.57, - 135365.00, - 3683.979, - 0.10, + 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 -@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( 'exchange_name, is_short, leverage, trading_mode, collateral, wallet_balance, ' '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, 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) assert isclose(round(exchange.liquidation_price( open_rate=open_rate, is_short=is_short, - leverage=leverage, - trading_mode=trading_mode, - collateral=collateral, wallet_balance=wallet_balance, mm_ex_1=mm_ex_1, upnl_ex_1=upnl_ex_1,