From a087d03db98a9510ffa31c7b65d83f9a9fc9f50e Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Fri, 6 Aug 2021 01:15:18 -0600 Subject: [PATCH 01/53] Added liquidation_price function --- freqtrade/leverage/__init__.py | 1 + freqtrade/leverage/liquidation_price.py | 133 ++++++++++++++++++++++++ freqtrade/persistence/models.py | 21 +++- tests/leverage/test_leverage.py | 89 ++++++++++++++++ tests/test_persistence.py | 10 +- 5 files changed, 246 insertions(+), 8 deletions(-) create mode 100644 freqtrade/leverage/liquidation_price.py create mode 100644 tests/leverage/test_leverage.py diff --git a/freqtrade/leverage/__init__.py b/freqtrade/leverage/__init__.py index ae78f4722..0bb2dd0be 100644 --- a/freqtrade/leverage/__init__.py +++ b/freqtrade/leverage/__init__.py @@ -1,2 +1,3 @@ # flake8: noqa: F401 from freqtrade.leverage.interest import interest +from freqtrade.leverage.liquidation_price import liquidation_price diff --git a/freqtrade/leverage/liquidation_price.py b/freqtrade/leverage/liquidation_price.py new file mode 100644 index 000000000..98ceb1704 --- /dev/null +++ b/freqtrade/leverage/liquidation_price.py @@ -0,0 +1,133 @@ +from typing import Optional + +from freqtrade.enums import Collateral, TradingMode +from freqtrade.exceptions import OperationalException + + +def liquidation_price( + exchange_name: str, + open_rate: float, + is_short: bool, + leverage: float, + trading_mode: TradingMode, + collateral: Optional[Collateral] +) -> Optional[float]: + + leverage_exchanges = [ + 'binance', + 'kraken', + 'ftx' + ] + if trading_mode == TradingMode.SPOT or exchange_name.lower() not in leverage_exchanges: + return None + + if not collateral: + raise OperationalException( + "Parameter collateral is required by liquidation_price when trading_mode is " + f"{trading_mode}" + ) + + if exchange_name.lower() == "binance": + return binance(open_rate, is_short, leverage, trading_mode, collateral) + elif exchange_name.lower() == "kraken": + return kraken(open_rate, is_short, leverage, trading_mode, collateral) + elif exchange_name.lower() == "ftx": + return ftx(open_rate, is_short, leverage, trading_mode, collateral) + raise OperationalException( + f"liquidation_price is not yet implemented for {exchange_name}" + ) + + +def exception( + exchange: str, + trading_mode: TradingMode, + collateral: Collateral +): + """ + Raises an exception if exchange used doesn't support desired leverage mode + :param name: Name of the exchange + :param trading_mode: spot, margin, futures + :param collateral: cross, isolated + """ + raise OperationalException( + f"{exchange} does not support {collateral.value} {trading_mode.value} trading") + + +def binance( + open_rate: float, + is_short: bool, + leverage: float, + trading_mode: TradingMode, + collateral: Collateral +): + """ + Calculates the liquidation price on Binance + :param name: Name of the exchange + :param trading_mode: spot, margin, futures + :param collateral: cross, isolated + """ + # TODO-lev: Additional arguments, fill in formulas + + if trading_mode == TradingMode.MARGIN and collateral == Collateral.CROSS: + # TODO-lev: perform a calculation based on this formula + # https://www.binance.com/en/support/faq/f6b010588e55413aa58b7d63ee0125ed + exception("binance", trading_mode, collateral) + elif trading_mode == TradingMode.FUTURES and collateral == Collateral.CROSS: + # TODO-lev: perform a calculation based on this formula + # https://www.binance.com/en/support/faq/b3c689c1f50a44cabb3a84e663b81d93 + exception("binance", trading_mode, collateral) + elif trading_mode == TradingMode.FUTURES and collateral == Collateral.ISOLATED: + # TODO-lev: perform a calculation based on this formula + # https://www.binance.com/en/support/faq/b3c689c1f50a44cabb3a84e663b81d93 + exception("binance", trading_mode, collateral) + + # If nothing was returned + exception("binance", trading_mode, collateral) + + +def kraken( + open_rate: float, + is_short: bool, + leverage: float, + trading_mode: TradingMode, + collateral: Collateral +): + """ + Calculates the liquidation price on Kraken + :param name: Name of the exchange + :param trading_mode: spot, margin, futures + :param collateral: cross, isolated + """ + # TODO-lev: Additional arguments, fill in formulas + + if collateral == Collateral.CROSS: + if trading_mode == TradingMode.MARGIN: + exception("kraken", trading_mode, collateral) + # TODO-lev: perform a calculation based on this formula + # https://support.kraken.com/hc/en-us/articles/203325763-Margin-Call-Level-and-Margin-Liquidation-Level + elif trading_mode == TradingMode.FUTURES: + exception("kraken", trading_mode, collateral) + + # If nothing was returned + exception("kraken", trading_mode, collateral) + + +def ftx( + open_rate: float, + is_short: bool, + leverage: float, + trading_mode: TradingMode, + collateral: Collateral +): + """ + Calculates the liquidation price on FTX + :param name: Name of the exchange + :param trading_mode: spot, margin, futures + :param collateral: cross, isolated + """ + if collateral == Collateral.CROSS: + # TODO-lev: Additional arguments, fill in formulas + exception("ftx", trading_mode, collateral) + + # If nothing was returned + exception("ftx", trading_mode, collateral) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 7c65fbc86..cd22d9ead 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -14,9 +14,10 @@ from sqlalchemy.pool import StaticPool from sqlalchemy.sql.schema import UniqueConstraint from freqtrade.constants import DATETIME_PRINT_FORMAT, NON_OPEN_EXCHANGE_STATES -from freqtrade.enums import SellType, TradingMode +from freqtrade.enums import Collateral, SellType, TradingMode from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.leverage import interest +from freqtrade.leverage import liquidation_price from freqtrade.misc import safe_value_fallback from freqtrade.persistence.migrations import check_migrate @@ -333,7 +334,7 @@ class LocalTrade(): for key in kwargs: setattr(self, key, kwargs[key]) if self.isolated_liq: - self.set_isolated_liq(self.isolated_liq) + self.set_isolated_liq(isolated_liq=self.isolated_liq) self.recalc_open_trade_value() if self.trading_mode == TradingMode.MARGIN and self.interest_rate is None: raise OperationalException( @@ -362,11 +363,25 @@ 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: Optional[float]): """ Method you should use to set self.liquidation price. Assures stop_loss is not passed the liquidation price """ + if not isolated_liq: + isolated_liq = liquidation_price( + exchange_name=self.exchange, + open_rate=self.open_rate, + is_short=self.is_short, + leverage=self.leverage, + trading_mode=self.trading_mode, + collateral=Collateral.ISOLATED + ) + if isolated_liq is None: + raise OperationalException( + "leverage/isolated_liq returned None. This exception should never happen" + ) + if self.stop_loss is not None: if self.is_short: self.stop_loss = min(self.stop_loss, isolated_liq) diff --git a/tests/leverage/test_leverage.py b/tests/leverage/test_leverage.py new file mode 100644 index 000000000..3a9fe9486 --- /dev/null +++ b/tests/leverage/test_leverage.py @@ -0,0 +1,89 @@ +# from decimal import Decimal + +from freqtrade.enums import Collateral, TradingMode +from freqtrade.leverage import liquidation_price + + +# from freqtrade.exceptions import OperationalException +binance = "binance" +kraken = "kraken" +ftx = "ftx" +other = "bittrex" + + +def test_liquidation_price(): + + spot = TradingMode.SPOT + margin = TradingMode.MARGIN + futures = TradingMode.FUTURES + + cross = Collateral.CROSS + isolated = Collateral.ISOLATED + + # NONE + assert liquidation_price(exchange_name=other, trading_mode=spot) is None + assert liquidation_price(exchange_name=other, trading_mode=margin, + collateral=cross) is None + assert liquidation_price(exchange_name=other, trading_mode=margin, + collateral=isolated) is None + assert liquidation_price( + exchange_name=other, trading_mode=futures, collateral=cross) is None + assert liquidation_price(exchange_name=other, trading_mode=futures, + collateral=isolated) is None + + # Binance + assert liquidation_price(exchange_name=binance, trading_mode=spot) is None + assert liquidation_price(exchange_name=binance, trading_mode=spot, + collateral=cross) is None + assert liquidation_price(exchange_name=binance, trading_mode=spot, + collateral=isolated) is None + # TODO-lev: Uncomment these assertions and make them real calculation tests + # TODO-lev: Replace 1.0 with real value + # assert liquidation_price( + # exchange_name=binance, + # trading_mode=margin, + # collateral=cross + # ) == 1.0 + # assert liquidation_price( + # exchange_name=binance, + # trading_mode=margin, + # collateral=isolated + # ) == 1.0 + # assert liquidation_price( + # exchange_name=binance, + # trading_mode=futures, + # collateral=cross + # ) == 1.0 + + # Binance supports isolated margin, but freqtrade likely won't for a while on Binance + # liquidation_price(exchange_name=binance, trading_mode=margin, collateral=isolated) + # assert exception thrown #TODO-lev: Check that exception is thrown + + # Kraken + assert liquidation_price(exchange_name=kraken, trading_mode=spot) is None + assert liquidation_price(exchange_name=kraken, trading_mode=spot, collateral=cross) is None + assert liquidation_price(exchange_name=kraken, trading_mode=spot, + collateral=isolated) is None + # TODO-lev: Uncomment these assertions and make them real calculation tests + # assert liquidation_price(kraken, trading_mode=margin, collateral=cross) == 1.0 + # assert liquidation_price(kraken, trading_mode=margin, collateral=isolated) == 1.0 + + # liquidation_price(kraken, trading_mode=futures, collateral=cross) + # assert exception thrown #TODO-lev: Check that exception is thrown + + # liquidation_price(kraken, trading_mode=futures, collateral=isolated) + # assert exception thrown #TODO-lev: Check that exception is thrown + + # FTX + assert liquidation_price(ftx, trading_mode=spot) is None + assert liquidation_price(ftx, trading_mode=spot, collateral=cross) is None + assert liquidation_price(ftx, trading_mode=spot, collateral=isolated) is None + # TODO-lev: Uncomment these assertions and make them real calculation tests + # assert liquidation_price(ftx, trading_mode=margin, collateral=cross) == 1.0 + # assert liquidation_price(ftx, trading_mode=margin, collateral=isolated) == 1.0 + + # liquidation_price(ftx, trading_mode=futures, collateral=cross) + # assert exception thrown #TODO-lev: Check that exception is thrown + + # liquidation_price(ftx, trading_mode=futures, collateral=isolated) + # assert exception thrown #TODO-lev: Check that exception is thrown diff --git a/tests/test_persistence.py b/tests/test_persistence.py index e28e3b2ed..9f9c2da19 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -109,7 +109,7 @@ def test_set_stop_loss_isolated_liq(fee): leverage=2.0, trading_mode=margin ) - trade.set_isolated_liq(0.09) + trade.set_isolated_liq(isolated_liq=0.09) assert trade.isolated_liq == 0.09 assert trade.stop_loss == 0.09 assert trade.initial_stop_loss == 0.09 @@ -119,12 +119,12 @@ def test_set_stop_loss_isolated_liq(fee): assert trade.stop_loss == 0.1 assert trade.initial_stop_loss == 0.09 - trade.set_isolated_liq(0.08) + trade.set_isolated_liq(isolated_liq=0.08) assert trade.isolated_liq == 0.08 assert trade.stop_loss == 0.1 assert trade.initial_stop_loss == 0.09 - trade.set_isolated_liq(0.11) + trade.set_isolated_liq(isolated_liq=0.11) assert trade.isolated_liq == 0.11 assert trade.stop_loss == 0.11 assert trade.initial_stop_loss == 0.09 @@ -1472,7 +1472,7 @@ def test_adjust_stop_loss_short(fee): assert trade.initial_stop_loss == 1.05 assert trade.initial_stop_loss_pct == 0.05 assert trade.stop_loss_pct == 0.1 - trade.set_isolated_liq(0.63) + trade.set_isolated_liq(isolated_liq=0.63) trade.adjust_stop_loss(0.59, -0.1) assert trade.stop_loss == 0.63 assert trade.isolated_liq == 0.63 @@ -1803,7 +1803,7 @@ def test_stoploss_reinitialization_short(default_conf, fee): assert trade_adj.initial_stop_loss == 1.04 assert trade_adj.initial_stop_loss_pct == 0.04 # Stoploss can't go above liquidation price - trade_adj.set_isolated_liq(1.0) + trade_adj.set_isolated_liq(isolated_liq=1.0) trade.adjust_stop_loss(0.97, -0.04) assert trade_adj.stop_loss == 1.0 assert trade_adj.stop_loss == 1.0 From fe5e00361e09e95437193abb6ee6cf01d574e7d5 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Wed, 25 Aug 2021 14:27:55 -0600 Subject: [PATCH 02/53] separated test_leverage into test_interest and test_liquidation_price, and paramaterized tests --- freqtrade/leverage/liquidation_price.py | 9 +- tests/leverage/test_leverage.py | 89 ------------------ tests/leverage/test_liquidation_price.py | 113 +++++++++++++++++++++++ 3 files changed, 115 insertions(+), 96 deletions(-) delete mode 100644 tests/leverage/test_leverage.py create mode 100644 tests/leverage/test_liquidation_price.py diff --git a/freqtrade/leverage/liquidation_price.py b/freqtrade/leverage/liquidation_price.py index 98ceb1704..62199a657 100644 --- a/freqtrade/leverage/liquidation_price.py +++ b/freqtrade/leverage/liquidation_price.py @@ -13,12 +13,7 @@ def liquidation_price( collateral: Optional[Collateral] ) -> Optional[float]: - leverage_exchanges = [ - 'binance', - 'kraken', - 'ftx' - ] - if trading_mode == TradingMode.SPOT or exchange_name.lower() not in leverage_exchanges: + if trading_mode == TradingMode.SPOT: return None if not collateral: @@ -34,7 +29,7 @@ def liquidation_price( elif exchange_name.lower() == "ftx": return ftx(open_rate, is_short, leverage, trading_mode, collateral) raise OperationalException( - f"liquidation_price is not yet implemented for {exchange_name}" + f"liquidation_price is not implemented for {exchange_name}" ) diff --git a/tests/leverage/test_leverage.py b/tests/leverage/test_leverage.py deleted file mode 100644 index 3a9fe9486..000000000 --- a/tests/leverage/test_leverage.py +++ /dev/null @@ -1,89 +0,0 @@ -# from decimal import Decimal - -from freqtrade.enums import Collateral, TradingMode -from freqtrade.leverage import liquidation_price - - -# from freqtrade.exceptions import OperationalException -binance = "binance" -kraken = "kraken" -ftx = "ftx" -other = "bittrex" - - -def test_liquidation_price(): - - spot = TradingMode.SPOT - margin = TradingMode.MARGIN - futures = TradingMode.FUTURES - - cross = Collateral.CROSS - isolated = Collateral.ISOLATED - - # NONE - assert liquidation_price(exchange_name=other, trading_mode=spot) is None - assert liquidation_price(exchange_name=other, trading_mode=margin, - collateral=cross) is None - assert liquidation_price(exchange_name=other, trading_mode=margin, - collateral=isolated) is None - assert liquidation_price( - exchange_name=other, trading_mode=futures, collateral=cross) is None - assert liquidation_price(exchange_name=other, trading_mode=futures, - collateral=isolated) is None - - # Binance - assert liquidation_price(exchange_name=binance, trading_mode=spot) is None - assert liquidation_price(exchange_name=binance, trading_mode=spot, - collateral=cross) is None - assert liquidation_price(exchange_name=binance, trading_mode=spot, - collateral=isolated) is None - # TODO-lev: Uncomment these assertions and make them real calculation tests - # TODO-lev: Replace 1.0 with real value - # assert liquidation_price( - # exchange_name=binance, - # trading_mode=margin, - # collateral=cross - # ) == 1.0 - # assert liquidation_price( - # exchange_name=binance, - # trading_mode=margin, - # collateral=isolated - # ) == 1.0 - # assert liquidation_price( - # exchange_name=binance, - # trading_mode=futures, - # collateral=cross - # ) == 1.0 - - # Binance supports isolated margin, but freqtrade likely won't for a while on Binance - # liquidation_price(exchange_name=binance, trading_mode=margin, collateral=isolated) - # assert exception thrown #TODO-lev: Check that exception is thrown - - # Kraken - assert liquidation_price(exchange_name=kraken, trading_mode=spot) is None - assert liquidation_price(exchange_name=kraken, trading_mode=spot, collateral=cross) is None - assert liquidation_price(exchange_name=kraken, trading_mode=spot, - collateral=isolated) is None - # TODO-lev: Uncomment these assertions and make them real calculation tests - # assert liquidation_price(kraken, trading_mode=margin, collateral=cross) == 1.0 - # assert liquidation_price(kraken, trading_mode=margin, collateral=isolated) == 1.0 - - # liquidation_price(kraken, trading_mode=futures, collateral=cross) - # assert exception thrown #TODO-lev: Check that exception is thrown - - # liquidation_price(kraken, trading_mode=futures, collateral=isolated) - # assert exception thrown #TODO-lev: Check that exception is thrown - - # FTX - assert liquidation_price(ftx, trading_mode=spot) is None - assert liquidation_price(ftx, trading_mode=spot, collateral=cross) is None - assert liquidation_price(ftx, trading_mode=spot, collateral=isolated) is None - # TODO-lev: Uncomment these assertions and make them real calculation tests - # assert liquidation_price(ftx, trading_mode=margin, collateral=cross) == 1.0 - # assert liquidation_price(ftx, trading_mode=margin, collateral=isolated) == 1.0 - - # liquidation_price(ftx, trading_mode=futures, collateral=cross) - # assert exception thrown #TODO-lev: Check that exception is thrown - - # liquidation_price(ftx, trading_mode=futures, collateral=isolated) - # assert exception thrown #TODO-lev: Check that exception is thrown diff --git a/tests/leverage/test_liquidation_price.py b/tests/leverage/test_liquidation_price.py new file mode 100644 index 000000000..687dd57f4 --- /dev/null +++ b/tests/leverage/test_liquidation_price.py @@ -0,0 +1,113 @@ +import pytest + +from freqtrade.enums import Collateral, TradingMode +from freqtrade.leverage import liquidation_price + + +# from freqtrade.exceptions import OperationalException + +spot = TradingMode.SPOT +margin = TradingMode.MARGIN +futures = TradingMode.FUTURES + +cross = Collateral.CROSS +isolated = Collateral.ISOLATED + + +@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), + # 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), + # Kraken + ('kraken', "2.0", False, "3.0", spot, None), + ('kraken', "2.0", True, "3.0", spot, cross), + ('kraken', "2.0", False, "1.0", spot, isolated), + # FTX + ('ftx', "2.0", True, "3.0", spot, None), + ('ftx', "2.0", False, "3.0", spot, cross), + ('ftx', "2.0", False, "3.0", spot, isolated), +]) +def test_liquidation_price_is_none( + exchange_name, + open_rate, + is_short, + leverage, + trading_mode, + collateral +): + assert liquidation_price( + exchange_name, + open_rate, + is_short, + leverage, + trading_mode, + collateral + ) 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", False, "1.0", margin, isolated), + ('kraken', "2.0", False, "1.0", futures, isolated), + # FTX + ('ftx', "2.0", False, "3.0", margin, isolated), + ('ftx', "2.0", False, "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,open_rate,is_short,leverage,trading_mode,collateral,result', [ + # Binance + ('binance', "2.0", False, "1.0", margin, cross, 1.0), + ('binance', "2.0", False, "1.0", futures, cross, 1.0), + ('binance', "2.0", False, "1.0", futures, isolated, 1.0), + # Kraken + ('kraken', "2.0", True, "3.0", margin, cross, 1.0), + ('kraken', "2.0", True, "3.0", futures, cross, 1.0), + # FTX + ('ftx', "2.0", False, "3.0", margin, cross, 1.0), + ('ftx', "2.0", False, "3.0", futures, cross, 1.0), + ] +) +def test_liquidation_price( + exchange_name, + open_rate, + is_short, + leverage, + trading_mode, + collateral, + result +): + # assert liquidation_price( + # exchange_name, + # open_rate, + # is_short, + # leverage, + # trading_mode, + # collateral + # ) == result + return # Here to avoid indent error From 60454334d9526b1c39f2165b18ef861594444b2b Mon Sep 17 00:00:00 2001 From: Arunavo Ray Date: Fri, 27 Aug 2021 11:28:12 +0530 Subject: [PATCH 03/53] =?UTF-8?q?Added=20Formulas=20to=20Calculate=20Liqui?= =?UTF-8?q?dation=20Price=20of=20Binance=20USD=E2=93=88-M=20Futures=20Cont?= =?UTF-8?q?racts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- freqtrade/enums/__init__.py | 1 + freqtrade/enums/marginmode.py | 10 ++ freqtrade/leverage/liquidation_price.py | 151 ++++++++++++++++++++---- 3 files changed, 140 insertions(+), 22 deletions(-) create mode 100644 freqtrade/enums/marginmode.py diff --git a/freqtrade/enums/__init__.py b/freqtrade/enums/__init__.py index 4eb0ce307..9c6788f6c 100644 --- a/freqtrade/enums/__init__.py +++ b/freqtrade/enums/__init__.py @@ -9,3 +9,4 @@ from freqtrade.enums.selltype import SellType from freqtrade.enums.signaltype import SignalDirection, SignalTagType, SignalType from freqtrade.enums.state import State from freqtrade.enums.tradingmode import TradingMode +from freqtrade.enums.marginmode import MarginMode diff --git a/freqtrade/enums/marginmode.py b/freqtrade/enums/marginmode.py new file mode 100644 index 000000000..80df6e6fa --- /dev/null +++ b/freqtrade/enums/marginmode.py @@ -0,0 +1,10 @@ +from enum import Enum + + +class MarginMode(Enum): + """ + Enum to distinguish between + one-way mode or hedge mode in Futures (Cross and Isolated) or Margin Trading + """ + ONE_WAY = "one-way" + HEDGE = "hedge" diff --git a/freqtrade/leverage/liquidation_price.py b/freqtrade/leverage/liquidation_price.py index 62199a657..383d598b4 100644 --- a/freqtrade/leverage/liquidation_price.py +++ b/freqtrade/leverage/liquidation_price.py @@ -1,6 +1,6 @@ from typing import Optional -from freqtrade.enums import Collateral, TradingMode +from freqtrade.enums import Collateral, TradingMode, MarginMode from freqtrade.exceptions import OperationalException @@ -12,7 +12,6 @@ def liquidation_price( trading_mode: TradingMode, collateral: Optional[Collateral] ) -> Optional[float]: - if trading_mode == TradingMode.SPOT: return None @@ -36,60 +35,164 @@ def liquidation_price( def exception( exchange: str, trading_mode: TradingMode, - collateral: Collateral + collateral: Collateral, + margin_mode: Optional[MarginMode] ): """ Raises an exception if exchange used doesn't support desired leverage mode - :param name: Name of the exchange + :param exchange: Name of the exchange + :param margin_mode: one-way or hedge :param trading_mode: spot, margin, futures :param collateral: cross, isolated """ + if not margin_mode: + raise OperationalException( + f"{exchange} does not support {collateral.value} {trading_mode.value} trading ") + raise OperationalException( - f"{exchange} does not support {collateral.value} {trading_mode.value} trading") + f"{exchange} does not support {collateral.value} {margin_mode.value} Mode {trading_mode.value} trading ") def binance( open_rate: float, is_short: bool, leverage: float, + margin_mode: MarginMode, trading_mode: TradingMode, - collateral: Collateral + collateral: Collateral, + **kwargs ): - """ + r""" Calculates the liquidation price on Binance - :param name: Name of the exchange + :param open_rate: open_rate + :param is_short: true or false + :param leverage: leverage in float + :param margin_mode: one-way or hedge :param trading_mode: spot, margin, futures :param collateral: cross, isolated + + :param \**kwargs: + See below + + :Keyword Arguments: + * *wallet_balance* (``float``) -- + Wallet Balance is crossWalletBalance in Cross-Margin Mode + Wallet Balance is isolatedWalletBalance in Isolated Margin Mode + + * *maintenance_margin_ex_1* (``float``) -- + Maintenance Margin of all other contracts, excluding Contract 1. + If it is an isolated margin mode, then TMM=0 + + * *unrealized_pnl_ex_1* (``float``) -- + Unrealized PNL of all other contracts, excluding Contract 1. + If it is an isolated margin mode, then UPNL=0 + + * *maintenance_amount_both* (``float``) -- + Maintenance Amount of BOTH position (one-way mode) + + * *maintenance_amount_long* (``float``) -- + Maintenance Amount of LONG position (hedge mode) + + * *maintenance_amount_short* (``float``) -- + Maintenance Amount of SHORT position (hedge mode) + + * *side_1_both* (``int``) -- + Direction of BOTH position, 1 as long position, -1 as short position + Derived from is_short + + * *position_1_both* (``float``) -- + Absolute value of BOTH position size (one-way mode) + + * *entry_price_1_both* (``float``) -- + Entry Price of BOTH position (one-way mode) + + * *position_1_long* (``float``) -- + Absolute value of LONG position size (hedge mode) + + * *entry_price_1_long* (``float``) -- + Entry Price of LONG position (hedge mode) + + * *position_1_short* (``float``) -- + Absolute value of SHORT position size (hedge mode) + + * *entry_price_1_short* (``float``) -- + Entry Price of SHORT position (hedge mode) + + * *maintenance_margin_rate_both* (``float``) -- + Maintenance margin rate of BOTH position (one-way mode) + + * *maintenance_margin_rate_long* (``float``) -- + Maintenance margin rate of LONG position (hedge mode) + + * *maintenance_margin_rate_short* (``float``) -- + Maintenance margin rate of SHORT position (hedge mode) """ # TODO-lev: Additional arguments, fill in formulas + wb = kwargs.get("wallet_balance") + tmm_1 = 0.0 if collateral == Collateral.ISOLATED else kwargs.get("maintenance_margin_ex_1") + upnl_1 = 0.0 if collateral == Collateral.ISOLATED else kwargs.get("unrealized_pnl_ex_1") + cum_b = kwargs.get("maintenance_amount_both") + cum_l = kwargs.get("maintenance_amount_long") + cum_s = kwargs.get("maintenance_amount_short") + side_1_both = -1 if is_short else 1 + position_1_both = abs(kwargs.get("position_1_both")) + ep1_both = kwargs.get("entry_price_1_both") + position_1_long = abs(kwargs.get("position_1_long")) + ep1_long = kwargs.get("entry_price_1_long") + position_1_short = abs(kwargs.get("position_1_short")) + ep1_short = kwargs.get("entry_price_1_short") + mmr_b = kwargs.get("maintenance_margin_rate_both") + mmr_l = kwargs.get("maintenance_margin_rate_long") + mmr_s = kwargs.get("maintenance_margin_rate_short") if trading_mode == TradingMode.MARGIN and collateral == Collateral.CROSS: # TODO-lev: perform a calculation based on this formula # https://www.binance.com/en/support/faq/f6b010588e55413aa58b7d63ee0125ed - exception("binance", trading_mode, collateral) - elif trading_mode == TradingMode.FUTURES and collateral == Collateral.CROSS: - # TODO-lev: perform a calculation based on this formula - # https://www.binance.com/en/support/faq/b3c689c1f50a44cabb3a84e663b81d93 - exception("binance", trading_mode, collateral) + exception("binance", trading_mode, collateral, margin_mode) elif trading_mode == TradingMode.FUTURES and collateral == Collateral.ISOLATED: - # TODO-lev: perform a calculation based on this formula # https://www.binance.com/en/support/faq/b3c689c1f50a44cabb3a84e663b81d93 - exception("binance", trading_mode, collateral) + # Liquidation Price of USDⓈ-M Futures Contracts Isolated + + if margin_mode == MarginMode.HEDGE: + exception("binance", trading_mode, collateral, margin_mode) + + elif margin_mode == MarginMode.ONE_WAY: + # Isolated margin mode, then TMM=0,UPNL=0 + return (wb + cum_b - (side_1_both * position_1_both * ep1_both)) / ( + position_1_both * mmr_b - side_1_both * position_1_both) + + elif trading_mode == TradingMode.FUTURES and collateral == Collateral.CROSS: + # https://www.binance.com/en/support/faq/b3c689c1f50a44cabb3a84e663b81d93 + # Liquidation Price of USDⓈ-M Futures Contracts Cross + + if margin_mode == MarginMode.HEDGE: + return (wb - tmm_1 + upnl_1 + cum_l + cum_s - (position_1_long * ep1_long) + ( + position_1_short * ep1_short)) / ( + position_1_long * mmr_l + position_1_short * mmr_s - position_1_long + position_1_short) + + elif margin_mode == MarginMode.ONE_WAY: + # Isolated margin mode, then TMM=0,UPNL=0 + return (wb - tmm_1 + upnl_1 + cum_b - (side_1_both * position_1_both * ep1_both)) / ( + position_1_both * mmr_b - side_1_both * position_1_both) # If nothing was returned - exception("binance", trading_mode, collateral) + exception("binance", trading_mode, collateral, margin_mode) def kraken( open_rate: float, is_short: bool, leverage: float, + margin_mode: MarginMode, trading_mode: TradingMode, collateral: Collateral ): """ Calculates the liquidation price on Kraken - :param name: Name of the exchange + :param open_rate: open_rate + :param is_short: true or false + :param leverage: leverage in float + :param margin_mode: one-way or hedge :param trading_mode: spot, margin, futures :param collateral: cross, isolated """ @@ -101,28 +204,32 @@ def kraken( # TODO-lev: perform a calculation based on this formula # https://support.kraken.com/hc/en-us/articles/203325763-Margin-Call-Level-and-Margin-Liquidation-Level elif trading_mode == TradingMode.FUTURES: - exception("kraken", trading_mode, collateral) + exception("kraken", trading_mode, collateral, margin_mode) # If nothing was returned - exception("kraken", trading_mode, collateral) + exception("kraken", trading_mode, collateral, margin_mode) def ftx( open_rate: float, is_short: bool, leverage: float, + margin_mode: MarginMode, trading_mode: TradingMode, collateral: Collateral ): """ Calculates the liquidation price on FTX - :param name: Name of the exchange + :param open_rate: open_rate + :param is_short: true or false + :param leverage: leverage in float + :param margin_mode: one-way or hedge :param trading_mode: spot, margin, futures :param collateral: cross, isolated """ if collateral == Collateral.CROSS: # TODO-lev: Additional arguments, fill in formulas - exception("ftx", trading_mode, collateral) + exception("ftx", trading_mode, collateral, margin_mode) # If nothing was returned - exception("ftx", trading_mode, collateral) + exception("ftx", trading_mode, collateral, margin_mode) From 1299cff8948e8f0f49435ef85981b892c68bec07 Mon Sep 17 00:00:00 2001 From: Arunavo Ray Date: Fri, 27 Aug 2021 11:54:51 +0530 Subject: [PATCH 04/53] Added Margin Mode Check for Binance. --- freqtrade/leverage/liquidation_price.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/freqtrade/leverage/liquidation_price.py b/freqtrade/leverage/liquidation_price.py index 383d598b4..21a699d40 100644 --- a/freqtrade/leverage/liquidation_price.py +++ b/freqtrade/leverage/liquidation_price.py @@ -10,7 +10,8 @@ def liquidation_price( is_short: bool, leverage: float, trading_mode: TradingMode, - collateral: Optional[Collateral] + collateral: Optional[Collateral], + margin_mode: Optional[MarginMode] ) -> Optional[float]: if trading_mode == TradingMode.SPOT: return None @@ -22,7 +23,11 @@ def liquidation_price( ) if exchange_name.lower() == "binance": - return binance(open_rate, is_short, leverage, trading_mode, collateral) + if not margin_mode: + raise OperationalException( + f"Parameter margin_mode is required by liquidation_price when exchange is {trading_mode}") + + return binance(open_rate, is_short, leverage, margin_mode, trading_mode, collateral) elif exchange_name.lower() == "kraken": return kraken(open_rate, is_short, leverage, trading_mode, collateral) elif exchange_name.lower() == "ftx": @@ -36,7 +41,7 @@ def exception( exchange: str, trading_mode: TradingMode, collateral: Collateral, - margin_mode: Optional[MarginMode] + margin_mode: Optional[MarginMode] = None ): """ Raises an exception if exchange used doesn't support desired leverage mode @@ -183,7 +188,6 @@ def kraken( open_rate: float, is_short: bool, leverage: float, - margin_mode: MarginMode, trading_mode: TradingMode, collateral: Collateral ): @@ -192,7 +196,6 @@ def kraken( :param open_rate: open_rate :param is_short: true or false :param leverage: leverage in float - :param margin_mode: one-way or hedge :param trading_mode: spot, margin, futures :param collateral: cross, isolated """ @@ -204,17 +207,16 @@ def kraken( # TODO-lev: perform a calculation based on this formula # https://support.kraken.com/hc/en-us/articles/203325763-Margin-Call-Level-and-Margin-Liquidation-Level elif trading_mode == TradingMode.FUTURES: - exception("kraken", trading_mode, collateral, margin_mode) + exception("kraken", trading_mode, collateral) # If nothing was returned - exception("kraken", trading_mode, collateral, margin_mode) + exception("kraken", trading_mode, collateral) def ftx( open_rate: float, is_short: bool, leverage: float, - margin_mode: MarginMode, trading_mode: TradingMode, collateral: Collateral ): @@ -223,13 +225,12 @@ def ftx( :param open_rate: open_rate :param is_short: true or false :param leverage: leverage in float - :param margin_mode: one-way or hedge :param trading_mode: spot, margin, futures :param collateral: cross, isolated """ if collateral == Collateral.CROSS: # TODO-lev: Additional arguments, fill in formulas - exception("ftx", trading_mode, collateral, margin_mode) + exception("ftx", trading_mode, collateral) # If nothing was returned - exception("ftx", trading_mode, collateral, margin_mode) + exception("ftx", trading_mode, collateral) From b30458f871f5abf0e730566384b6d01b873b31e5 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sun, 19 Sep 2021 17:16:59 -0600 Subject: [PATCH 05/53] Revert "Added Margin Mode Check for Binance." This reverts commit abcb9729e5a1466d2e733d83503fca05d0bd27c6. --- freqtrade/leverage/liquidation_price.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/freqtrade/leverage/liquidation_price.py b/freqtrade/leverage/liquidation_price.py index 21a699d40..383d598b4 100644 --- a/freqtrade/leverage/liquidation_price.py +++ b/freqtrade/leverage/liquidation_price.py @@ -10,8 +10,7 @@ def liquidation_price( is_short: bool, leverage: float, trading_mode: TradingMode, - collateral: Optional[Collateral], - margin_mode: Optional[MarginMode] + collateral: Optional[Collateral] ) -> Optional[float]: if trading_mode == TradingMode.SPOT: return None @@ -23,11 +22,7 @@ def liquidation_price( ) if exchange_name.lower() == "binance": - if not margin_mode: - raise OperationalException( - f"Parameter margin_mode is required by liquidation_price when exchange is {trading_mode}") - - return binance(open_rate, is_short, leverage, margin_mode, trading_mode, collateral) + return binance(open_rate, is_short, leverage, trading_mode, collateral) elif exchange_name.lower() == "kraken": return kraken(open_rate, is_short, leverage, trading_mode, collateral) elif exchange_name.lower() == "ftx": @@ -41,7 +36,7 @@ def exception( exchange: str, trading_mode: TradingMode, collateral: Collateral, - margin_mode: Optional[MarginMode] = None + margin_mode: Optional[MarginMode] ): """ Raises an exception if exchange used doesn't support desired leverage mode @@ -188,6 +183,7 @@ def kraken( open_rate: float, is_short: bool, leverage: float, + margin_mode: MarginMode, trading_mode: TradingMode, collateral: Collateral ): @@ -196,6 +192,7 @@ def kraken( :param open_rate: open_rate :param is_short: true or false :param leverage: leverage in float + :param margin_mode: one-way or hedge :param trading_mode: spot, margin, futures :param collateral: cross, isolated """ @@ -207,16 +204,17 @@ def kraken( # TODO-lev: perform a calculation based on this formula # https://support.kraken.com/hc/en-us/articles/203325763-Margin-Call-Level-and-Margin-Liquidation-Level elif trading_mode == TradingMode.FUTURES: - exception("kraken", trading_mode, collateral) + exception("kraken", trading_mode, collateral, margin_mode) # If nothing was returned - exception("kraken", trading_mode, collateral) + exception("kraken", trading_mode, collateral, margin_mode) def ftx( open_rate: float, is_short: bool, leverage: float, + margin_mode: MarginMode, trading_mode: TradingMode, collateral: Collateral ): @@ -225,12 +223,13 @@ def ftx( :param open_rate: open_rate :param is_short: true or false :param leverage: leverage in float + :param margin_mode: one-way or hedge :param trading_mode: spot, margin, futures :param collateral: cross, isolated """ if collateral == Collateral.CROSS: # TODO-lev: Additional arguments, fill in formulas - exception("ftx", trading_mode, collateral) + exception("ftx", trading_mode, collateral, margin_mode) # If nothing was returned - exception("ftx", trading_mode, collateral) + exception("ftx", trading_mode, collateral, margin_mode) From 80f4bae3fe8bc96bf45fadbbde8633c93342925d Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sun, 19 Sep 2021 17:17:09 -0600 Subject: [PATCH 06/53] =?UTF-8?q?Revert=20"Added=20Formulas=20to=20Calcula?= =?UTF-8?q?te=20Liquidation=20Price=20of=20Binance=20USD=E2=93=88-M=20Futu?= =?UTF-8?q?res=20Contracts"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit d343e84507c71a42bb0303c5b030c151c19f86e8. --- freqtrade/enums/__init__.py | 1 - freqtrade/enums/marginmode.py | 10 -- freqtrade/leverage/liquidation_price.py | 151 ++++-------------------- 3 files changed, 22 insertions(+), 140 deletions(-) delete mode 100644 freqtrade/enums/marginmode.py diff --git a/freqtrade/enums/__init__.py b/freqtrade/enums/__init__.py index 9c6788f6c..4eb0ce307 100644 --- a/freqtrade/enums/__init__.py +++ b/freqtrade/enums/__init__.py @@ -9,4 +9,3 @@ from freqtrade.enums.selltype import SellType from freqtrade.enums.signaltype import SignalDirection, SignalTagType, SignalType from freqtrade.enums.state import State from freqtrade.enums.tradingmode import TradingMode -from freqtrade.enums.marginmode import MarginMode diff --git a/freqtrade/enums/marginmode.py b/freqtrade/enums/marginmode.py deleted file mode 100644 index 80df6e6fa..000000000 --- a/freqtrade/enums/marginmode.py +++ /dev/null @@ -1,10 +0,0 @@ -from enum import Enum - - -class MarginMode(Enum): - """ - Enum to distinguish between - one-way mode or hedge mode in Futures (Cross and Isolated) or Margin Trading - """ - ONE_WAY = "one-way" - HEDGE = "hedge" diff --git a/freqtrade/leverage/liquidation_price.py b/freqtrade/leverage/liquidation_price.py index 383d598b4..62199a657 100644 --- a/freqtrade/leverage/liquidation_price.py +++ b/freqtrade/leverage/liquidation_price.py @@ -1,6 +1,6 @@ from typing import Optional -from freqtrade.enums import Collateral, TradingMode, MarginMode +from freqtrade.enums import Collateral, TradingMode from freqtrade.exceptions import OperationalException @@ -12,6 +12,7 @@ def liquidation_price( trading_mode: TradingMode, collateral: Optional[Collateral] ) -> Optional[float]: + if trading_mode == TradingMode.SPOT: return None @@ -35,164 +36,60 @@ def liquidation_price( def exception( exchange: str, trading_mode: TradingMode, - collateral: Collateral, - margin_mode: Optional[MarginMode] + collateral: Collateral ): """ Raises an exception if exchange used doesn't support desired leverage mode - :param exchange: Name of the exchange - :param margin_mode: one-way or hedge + :param name: Name of the exchange :param trading_mode: spot, margin, futures :param collateral: cross, isolated """ - if not margin_mode: - raise OperationalException( - f"{exchange} does not support {collateral.value} {trading_mode.value} trading ") - raise OperationalException( - f"{exchange} does not support {collateral.value} {margin_mode.value} Mode {trading_mode.value} trading ") + f"{exchange} does not support {collateral.value} {trading_mode.value} trading") def binance( open_rate: float, is_short: bool, leverage: float, - margin_mode: MarginMode, trading_mode: TradingMode, - collateral: Collateral, - **kwargs + collateral: Collateral ): - r""" + """ Calculates the liquidation price on Binance - :param open_rate: open_rate - :param is_short: true or false - :param leverage: leverage in float - :param margin_mode: one-way or hedge + :param name: Name of the exchange :param trading_mode: spot, margin, futures :param collateral: cross, isolated - - :param \**kwargs: - See below - - :Keyword Arguments: - * *wallet_balance* (``float``) -- - Wallet Balance is crossWalletBalance in Cross-Margin Mode - Wallet Balance is isolatedWalletBalance in Isolated Margin Mode - - * *maintenance_margin_ex_1* (``float``) -- - Maintenance Margin of all other contracts, excluding Contract 1. - If it is an isolated margin mode, then TMM=0 - - * *unrealized_pnl_ex_1* (``float``) -- - Unrealized PNL of all other contracts, excluding Contract 1. - If it is an isolated margin mode, then UPNL=0 - - * *maintenance_amount_both* (``float``) -- - Maintenance Amount of BOTH position (one-way mode) - - * *maintenance_amount_long* (``float``) -- - Maintenance Amount of LONG position (hedge mode) - - * *maintenance_amount_short* (``float``) -- - Maintenance Amount of SHORT position (hedge mode) - - * *side_1_both* (``int``) -- - Direction of BOTH position, 1 as long position, -1 as short position - Derived from is_short - - * *position_1_both* (``float``) -- - Absolute value of BOTH position size (one-way mode) - - * *entry_price_1_both* (``float``) -- - Entry Price of BOTH position (one-way mode) - - * *position_1_long* (``float``) -- - Absolute value of LONG position size (hedge mode) - - * *entry_price_1_long* (``float``) -- - Entry Price of LONG position (hedge mode) - - * *position_1_short* (``float``) -- - Absolute value of SHORT position size (hedge mode) - - * *entry_price_1_short* (``float``) -- - Entry Price of SHORT position (hedge mode) - - * *maintenance_margin_rate_both* (``float``) -- - Maintenance margin rate of BOTH position (one-way mode) - - * *maintenance_margin_rate_long* (``float``) -- - Maintenance margin rate of LONG position (hedge mode) - - * *maintenance_margin_rate_short* (``float``) -- - Maintenance margin rate of SHORT position (hedge mode) """ # TODO-lev: Additional arguments, fill in formulas - wb = kwargs.get("wallet_balance") - tmm_1 = 0.0 if collateral == Collateral.ISOLATED else kwargs.get("maintenance_margin_ex_1") - upnl_1 = 0.0 if collateral == Collateral.ISOLATED else kwargs.get("unrealized_pnl_ex_1") - cum_b = kwargs.get("maintenance_amount_both") - cum_l = kwargs.get("maintenance_amount_long") - cum_s = kwargs.get("maintenance_amount_short") - side_1_both = -1 if is_short else 1 - position_1_both = abs(kwargs.get("position_1_both")) - ep1_both = kwargs.get("entry_price_1_both") - position_1_long = abs(kwargs.get("position_1_long")) - ep1_long = kwargs.get("entry_price_1_long") - position_1_short = abs(kwargs.get("position_1_short")) - ep1_short = kwargs.get("entry_price_1_short") - mmr_b = kwargs.get("maintenance_margin_rate_both") - mmr_l = kwargs.get("maintenance_margin_rate_long") - mmr_s = kwargs.get("maintenance_margin_rate_short") if trading_mode == TradingMode.MARGIN and collateral == Collateral.CROSS: # TODO-lev: perform a calculation based on this formula # https://www.binance.com/en/support/faq/f6b010588e55413aa58b7d63ee0125ed - exception("binance", trading_mode, collateral, margin_mode) - elif trading_mode == TradingMode.FUTURES and collateral == Collateral.ISOLATED: - # https://www.binance.com/en/support/faq/b3c689c1f50a44cabb3a84e663b81d93 - # Liquidation Price of USDⓈ-M Futures Contracts Isolated - - if margin_mode == MarginMode.HEDGE: - exception("binance", trading_mode, collateral, margin_mode) - - elif margin_mode == MarginMode.ONE_WAY: - # Isolated margin mode, then TMM=0,UPNL=0 - return (wb + cum_b - (side_1_both * position_1_both * ep1_both)) / ( - position_1_both * mmr_b - side_1_both * position_1_both) - + exception("binance", trading_mode, collateral) elif trading_mode == TradingMode.FUTURES and collateral == Collateral.CROSS: + # TODO-lev: perform a calculation based on this formula # https://www.binance.com/en/support/faq/b3c689c1f50a44cabb3a84e663b81d93 - # Liquidation Price of USDⓈ-M Futures Contracts Cross - - if margin_mode == MarginMode.HEDGE: - return (wb - tmm_1 + upnl_1 + cum_l + cum_s - (position_1_long * ep1_long) + ( - position_1_short * ep1_short)) / ( - position_1_long * mmr_l + position_1_short * mmr_s - position_1_long + position_1_short) - - elif margin_mode == MarginMode.ONE_WAY: - # Isolated margin mode, then TMM=0,UPNL=0 - return (wb - tmm_1 + upnl_1 + cum_b - (side_1_both * position_1_both * ep1_both)) / ( - position_1_both * mmr_b - side_1_both * position_1_both) + exception("binance", trading_mode, collateral) + elif trading_mode == TradingMode.FUTURES and collateral == Collateral.ISOLATED: + # TODO-lev: perform a calculation based on this formula + # https://www.binance.com/en/support/faq/b3c689c1f50a44cabb3a84e663b81d93 + exception("binance", trading_mode, collateral) # If nothing was returned - exception("binance", trading_mode, collateral, margin_mode) + exception("binance", trading_mode, collateral) def kraken( open_rate: float, is_short: bool, leverage: float, - margin_mode: MarginMode, trading_mode: TradingMode, collateral: Collateral ): """ Calculates the liquidation price on Kraken - :param open_rate: open_rate - :param is_short: true or false - :param leverage: leverage in float - :param margin_mode: one-way or hedge + :param name: Name of the exchange :param trading_mode: spot, margin, futures :param collateral: cross, isolated """ @@ -204,32 +101,28 @@ def kraken( # TODO-lev: perform a calculation based on this formula # https://support.kraken.com/hc/en-us/articles/203325763-Margin-Call-Level-and-Margin-Liquidation-Level elif trading_mode == TradingMode.FUTURES: - exception("kraken", trading_mode, collateral, margin_mode) + exception("kraken", trading_mode, collateral) # If nothing was returned - exception("kraken", trading_mode, collateral, margin_mode) + exception("kraken", trading_mode, collateral) def ftx( open_rate: float, is_short: bool, leverage: float, - margin_mode: MarginMode, trading_mode: TradingMode, collateral: Collateral ): """ Calculates the liquidation price on FTX - :param open_rate: open_rate - :param is_short: true or false - :param leverage: leverage in float - :param margin_mode: one-way or hedge + :param name: Name of the exchange :param trading_mode: spot, margin, futures :param collateral: cross, isolated """ if collateral == Collateral.CROSS: # TODO-lev: Additional arguments, fill in formulas - exception("ftx", trading_mode, collateral, margin_mode) + exception("ftx", trading_mode, collateral) # If nothing was returned - exception("ftx", trading_mode, collateral, margin_mode) + exception("ftx", trading_mode, collateral) From 7119dc6e4117987e43034996f56916ed7bee8014 Mon Sep 17 00:00:00 2001 From: Arunavo Ray Date: Fri, 17 Sep 2021 08:17:15 +0530 Subject: [PATCH 07/53] Converted kwargs to params --- freqtrade/leverage/liquidation_price.py | 68 ++++++++++++++++++++++++- 1 file changed, 67 insertions(+), 1 deletion(-) diff --git a/freqtrade/leverage/liquidation_price.py b/freqtrade/leverage/liquidation_price.py index 62199a657..d05fbfe3a 100644 --- a/freqtrade/leverage/liquidation_price.py +++ b/freqtrade/leverage/liquidation_price.py @@ -53,15 +53,81 @@ def binance( is_short: bool, leverage: float, trading_mode: TradingMode, - collateral: Collateral + collateral: Collateral, + wallet_balance: float, + maintenance_margin_ex_1: float, + unrealized_pnl_ex_1: float, + maintenance_amount_both: float, + maintenance_amount_long: float, + maintenance_amount_short: float, + position_1_both: float, + entry_price_1_both: float, + position_1_long: float, + entry_price_1_long: float, + position_1_short: float, + entry_price_1_short: float, + maintenance_margin_rate_both: float, + maintenance_margin_rate_long: float, + maintenance_margin_rate_short: float, ): """ Calculates the liquidation price on Binance :param name: Name of the exchange :param trading_mode: spot, margin, futures :param collateral: cross, isolated + + :param wallet_balance: Wallet Balance is crossWalletBalance in Cross-Margin Mode. + Wallet Balance is isolatedWalletBalance in Isolated Margin Mode + + :param maintenance_margin_ex_1: Maintenance Margin of all other contracts, excluding Contract 1. + If it is an isolated margin mode, then TMM=0 + + :param unrealized_pnl_ex_1: Unrealized PNL of all other contracts, excluding Contract 1. + If it is an isolated margin mode, then UPNL=0 + + :param maintenance_amount_both: Maintenance Amount of BOTH position (one-way mode) + + :param maintenance_amount_long: Maintenance Amount of LONG position (hedge mode) + + :param maintenance_amount_short: Maintenance Amount of SHORT position (hedge mode) + + :param side_1_both: Direction of BOTH position, 1 as long position, -1 as short position derived from is_short + + :param position_1_both: Absolute value of BOTH position size (one-way mode) + + :param entry_price_1_both: Entry Price of BOTH position (one-way mode) + + :param position_1_long: Absolute value of LONG position size (hedge mode) + + :param entry_price_1_long: Entry Price of LONG position (hedge mode) + + :param position_1_short: Absolute value of SHORT position size (hedge mode) + + :param entry_price_1_short: Entry Price of SHORT position (hedge mode) + + :param maintenance_margin_rate_both: Maintenance margin rate of BOTH position (one-way mode) + + :param maintenance_margin_rate_long: Maintenance margin rate of LONG position (hedge mode) + + :param maintenance_margin_rate_short: Maintenance margin rate of SHORT position (hedge mode) """ # TODO-lev: Additional arguments, fill in formulas + wb = wallet_balance + tmm_1 = 0.0 if collateral == Collateral.ISOLATED else maintenance_margin_ex_1 + upnl_1 = 0.0 if collateral == Collateral.ISOLATED else unrealized_pnl_ex_1 + cum_b = maintenance_amount_both + cum_l = maintenance_amount_long + cum_s = maintenance_amount_short + side_1_both = -1 if is_short else 1 + position_1_both = abs(position_1_both) + ep1_both = entry_price_1_both + position_1_long = abs(position_1_long) + ep1_long = entry_price_1_long + position_1_short = abs(position_1_short) + ep1_short = entry_price_1_short + mmr_b = maintenance_margin_rate_both + mmr_l = maintenance_margin_rate_long + mmr_s = maintenance_margin_rate_short if trading_mode == TradingMode.MARGIN and collateral == Collateral.CROSS: # TODO-lev: perform a calculation based on this formula From f9a2d1a71d3edb121503311da120d41a27d40897 Mon Sep 17 00:00:00 2001 From: Arunavo Ray Date: Sun, 19 Sep 2021 10:18:28 +0530 Subject: [PATCH 08/53] Binance Liquidation Price Hedge-Mode Removed --- freqtrade/leverage/liquidation_price.py | 82 ++++++++++++------------- 1 file changed, 38 insertions(+), 44 deletions(-) diff --git a/freqtrade/leverage/liquidation_price.py b/freqtrade/leverage/liquidation_price.py index d05fbfe3a..12a2f0300 100644 --- a/freqtrade/leverage/liquidation_price.py +++ b/freqtrade/leverage/liquidation_price.py @@ -10,7 +10,14 @@ def liquidation_price( is_short: bool, leverage: float, trading_mode: TradingMode, - collateral: Optional[Collateral] + collateral: Optional[Collateral], + wallet_balance: Optional[float], + maintenance_margin_ex_1: Optional[float], + unrealized_pnl_ex_1: Optional[float], + maintenance_amount_both: Optional[float], + position_1_both: Optional[float], + entry_price_1_both: Optional[float], + maintenance_margin_rate_both: Optional[float] ) -> Optional[float]: if trading_mode == TradingMode.SPOT: @@ -23,7 +30,16 @@ def liquidation_price( ) if exchange_name.lower() == "binance": - return binance(open_rate, is_short, leverage, trading_mode, collateral) + if not wallet_balance or not maintenance_margin_ex_1 or not unrealized_pnl_ex_1 or not maintenance_amount_both \ + or not position_1_both or not entry_price_1_both or not maintenance_margin_rate_both: + raise OperationalException( + f"Parameters wallet_balance, maintenance_margin_ex_1, unrealized_pnl_ex_1, maintenance_amount_both, " + f"position_1_both, entry_price_1_both, maintenance_margin_rate_both is required by liquidation_price " + f"when exchange is {exchange_name.lower()}") + + return binance(open_rate, is_short, leverage, trading_mode, collateral, wallet_balance, maintenance_margin_ex_1, + unrealized_pnl_ex_1, maintenance_amount_both, position_1_both, entry_price_1_both, + maintenance_margin_rate_both) elif exchange_name.lower() == "kraken": return kraken(open_rate, is_short, leverage, trading_mode, collateral) elif exchange_name.lower() == "ftx": @@ -36,16 +52,17 @@ def liquidation_price( def exception( exchange: str, trading_mode: TradingMode, - collateral: Collateral + collateral: Collateral, ): """ Raises an exception if exchange used doesn't support desired leverage mode - :param name: Name of the exchange + :param exchange: Name of the exchange :param trading_mode: spot, margin, futures :param collateral: cross, isolated """ + raise OperationalException( - f"{exchange} does not support {collateral.value} {trading_mode.value} trading") + f"{exchange} does not support {collateral.value} Mode {trading_mode.value} trading ") def binance( @@ -58,21 +75,15 @@ def binance( maintenance_margin_ex_1: float, unrealized_pnl_ex_1: float, maintenance_amount_both: float, - maintenance_amount_long: float, - maintenance_amount_short: float, position_1_both: float, entry_price_1_both: float, - position_1_long: float, - entry_price_1_long: float, - position_1_short: float, - entry_price_1_short: float, maintenance_margin_rate_both: float, - maintenance_margin_rate_long: float, - maintenance_margin_rate_short: float, ): """ Calculates the liquidation price on Binance - :param name: Name of the exchange + :param open_rate: open_rate + :param is_short: true or false + :param leverage: leverage in float :param trading_mode: spot, margin, futures :param collateral: cross, isolated @@ -87,60 +98,43 @@ def binance( :param maintenance_amount_both: Maintenance Amount of BOTH position (one-way mode) - :param maintenance_amount_long: Maintenance Amount of LONG position (hedge mode) - - :param maintenance_amount_short: Maintenance Amount of SHORT position (hedge mode) - - :param side_1_both: Direction of BOTH position, 1 as long position, -1 as short position derived from is_short - :param position_1_both: Absolute value of BOTH position size (one-way mode) :param entry_price_1_both: Entry Price of BOTH position (one-way mode) - :param position_1_long: Absolute value of LONG position size (hedge mode) - - :param entry_price_1_long: Entry Price of LONG position (hedge mode) - - :param position_1_short: Absolute value of SHORT position size (hedge mode) - - :param entry_price_1_short: Entry Price of SHORT position (hedge mode) - :param maintenance_margin_rate_both: Maintenance margin rate of BOTH position (one-way mode) - :param maintenance_margin_rate_long: Maintenance margin rate of LONG position (hedge mode) - - :param maintenance_margin_rate_short: Maintenance margin rate of SHORT position (hedge mode) """ # TODO-lev: Additional arguments, fill in formulas wb = wallet_balance tmm_1 = 0.0 if collateral == Collateral.ISOLATED else maintenance_margin_ex_1 upnl_1 = 0.0 if collateral == Collateral.ISOLATED else unrealized_pnl_ex_1 cum_b = maintenance_amount_both - cum_l = maintenance_amount_long - cum_s = maintenance_amount_short side_1_both = -1 if is_short else 1 position_1_both = abs(position_1_both) ep1_both = entry_price_1_both - position_1_long = abs(position_1_long) - ep1_long = entry_price_1_long - position_1_short = abs(position_1_short) - ep1_short = entry_price_1_short mmr_b = maintenance_margin_rate_both - mmr_l = maintenance_margin_rate_long - mmr_s = maintenance_margin_rate_short if trading_mode == TradingMode.MARGIN and collateral == Collateral.CROSS: # TODO-lev: perform a calculation based on this formula # https://www.binance.com/en/support/faq/f6b010588e55413aa58b7d63ee0125ed exception("binance", trading_mode, collateral) + elif trading_mode == TradingMode.FUTURES and collateral == Collateral.ISOLATED: + # https://www.binance.com/en/support/faq/b3c689c1f50a44cabb3a84e663b81d93 + # Liquidation Price of USDⓈ-M Futures Contracts Isolated + + # Isolated margin mode, then TMM=0,UPNL=0 + return (wb + cum_b - (side_1_both * position_1_both * ep1_both)) / ( + position_1_both * mmr_b - side_1_both * position_1_both) + elif trading_mode == TradingMode.FUTURES and collateral == Collateral.CROSS: # TODO-lev: perform a calculation based on this formula # https://www.binance.com/en/support/faq/b3c689c1f50a44cabb3a84e663b81d93 - exception("binance", trading_mode, collateral) - elif trading_mode == TradingMode.FUTURES and collateral == Collateral.ISOLATED: - # TODO-lev: perform a calculation based on this formula - # https://www.binance.com/en/support/faq/b3c689c1f50a44cabb3a84e663b81d93 - exception("binance", trading_mode, collateral) + # Liquidation Price of USDⓈ-M Futures Contracts Cross + + # Isolated margin mode, then TMM=0,UPNL=0 + return (wb - tmm_1 + upnl_1 + cum_b - (side_1_both * position_1_both * ep1_both)) / ( + position_1_both * mmr_b - side_1_both * position_1_both) # If nothing was returned exception("binance", trading_mode, collateral) From 3709130eb791901f64c165e6f0ab64bb57b28af4 Mon Sep 17 00:00:00 2001 From: Arunavo Ray Date: Sun, 19 Sep 2021 11:16:41 +0530 Subject: [PATCH 09/53] Added Tests for Binance Liquidation price, shortened liquidation param names --- freqtrade/leverage/liquidation_price.py | 81 ++++++++++++------------ tests/leverage/test_liquidation_price.py | 74 ++++++++++++---------- 2 files changed, 83 insertions(+), 72 deletions(-) diff --git a/freqtrade/leverage/liquidation_price.py b/freqtrade/leverage/liquidation_price.py index 12a2f0300..a18649812 100644 --- a/freqtrade/leverage/liquidation_price.py +++ b/freqtrade/leverage/liquidation_price.py @@ -12,12 +12,12 @@ def liquidation_price( trading_mode: TradingMode, collateral: Optional[Collateral], wallet_balance: Optional[float], - maintenance_margin_ex_1: Optional[float], - unrealized_pnl_ex_1: Optional[float], - maintenance_amount_both: Optional[float], - position_1_both: Optional[float], - entry_price_1_both: Optional[float], - maintenance_margin_rate_both: Optional[float] + mm_ex_1: Optional[float], + upnl_ex_1: Optional[float], + maintenance_amt: Optional[float], + position: Optional[float], + entry_price: Optional[float], + mm_rate: Optional[float] ) -> Optional[float]: if trading_mode == TradingMode.SPOT: @@ -30,16 +30,17 @@ def liquidation_price( ) if exchange_name.lower() == "binance": - if not wallet_balance or not maintenance_margin_ex_1 or not unrealized_pnl_ex_1 or not maintenance_amount_both \ - or not position_1_both or not entry_price_1_both or not maintenance_margin_rate_both: + if not wallet_balance or not mm_ex_1 or not upnl_ex_1 \ + or not maintenance_amt or not position or not entry_price \ + or not mm_rate: raise OperationalException( - f"Parameters wallet_balance, maintenance_margin_ex_1, unrealized_pnl_ex_1, maintenance_amount_both, " - f"position_1_both, entry_price_1_both, maintenance_margin_rate_both is required by liquidation_price " - f"when exchange is {exchange_name.lower()}") + f"Parameters wallet_balance, mm_ex_1, upnl_ex_1, " + f"maintenance_amt, position, entry_price, mm_rate " + f"is required by liquidation_price when exchange is {exchange_name.lower()}") - return binance(open_rate, is_short, leverage, trading_mode, collateral, wallet_balance, maintenance_margin_ex_1, - unrealized_pnl_ex_1, maintenance_amount_both, position_1_both, entry_price_1_both, - maintenance_margin_rate_both) + return binance(open_rate, is_short, leverage, trading_mode, collateral, wallet_balance, + mm_ex_1, upnl_ex_1, maintenance_amt, + position, entry_price, mm_rate) elif exchange_name.lower() == "kraken": return kraken(open_rate, is_short, leverage, trading_mode, collateral) elif exchange_name.lower() == "ftx": @@ -72,12 +73,12 @@ def binance( trading_mode: TradingMode, collateral: Collateral, wallet_balance: float, - maintenance_margin_ex_1: float, - unrealized_pnl_ex_1: float, - maintenance_amount_both: float, - position_1_both: float, - entry_price_1_both: float, - maintenance_margin_rate_both: float, + mm_ex_1: float, + upnl_ex_1: float, + maintenance_amt: float, + position: float, + entry_price: float, + mm_rate: float, ): """ Calculates the liquidation price on Binance @@ -88,32 +89,32 @@ def binance( :param collateral: cross, isolated :param wallet_balance: Wallet Balance is crossWalletBalance in Cross-Margin Mode. - Wallet Balance is isolatedWalletBalance in Isolated Margin Mode + Wallet Balance is isolatedWalletBalance in Isolated Margin Mode - :param maintenance_margin_ex_1: Maintenance Margin of all other contracts, excluding Contract 1. - If it is an isolated margin mode, then TMM=0 + :param mm_ex_1: Maintenance Margin of all other contracts, + excluding Contract 1. If it is an isolated margin mode, then TMM=0 - :param unrealized_pnl_ex_1: Unrealized PNL of all other contracts, excluding Contract 1. - If it is an isolated margin mode, then UPNL=0 + :param upnl_ex_1: Unrealized PNL of all other contracts, excluding Contract 1. + If it is an isolated margin mode, then UPNL=0 - :param maintenance_amount_both: Maintenance Amount of BOTH position (one-way mode) + :param maintenance_amt: Maintenance Amount of position (one-way mode) - :param position_1_both: Absolute value of BOTH position size (one-way mode) + :param position: Absolute value of position size (one-way mode) - :param entry_price_1_both: Entry Price of BOTH position (one-way mode) + :param entry_price: Entry Price of position (one-way mode) - :param maintenance_margin_rate_both: Maintenance margin rate of BOTH position (one-way mode) + :param mm_rate: Maintenance margin rate of position (one-way mode) """ # TODO-lev: Additional arguments, fill in formulas wb = wallet_balance - tmm_1 = 0.0 if collateral == Collateral.ISOLATED else maintenance_margin_ex_1 - upnl_1 = 0.0 if collateral == Collateral.ISOLATED else unrealized_pnl_ex_1 - cum_b = maintenance_amount_both - side_1_both = -1 if is_short else 1 - position_1_both = abs(position_1_both) - ep1_both = entry_price_1_both - mmr_b = maintenance_margin_rate_both + tmm_1 = 0.0 if collateral == Collateral.ISOLATED else mm_ex_1 + upnl_1 = 0.0 if collateral == Collateral.ISOLATED else upnl_ex_1 + cum_b = maintenance_amt + side_1 = -1 if is_short else 1 + position = abs(position) + ep1 = entry_price + mmr_b = mm_rate if trading_mode == TradingMode.MARGIN and collateral == Collateral.CROSS: # TODO-lev: perform a calculation based on this formula @@ -124,8 +125,8 @@ def binance( # Liquidation Price of USDⓈ-M Futures Contracts Isolated # Isolated margin mode, then TMM=0,UPNL=0 - return (wb + cum_b - (side_1_both * position_1_both * ep1_both)) / ( - position_1_both * mmr_b - side_1_both * position_1_both) + return (wb + cum_b - side_1 * position * ep1) / ( + position * mmr_b - side_1 * position) elif trading_mode == TradingMode.FUTURES and collateral == Collateral.CROSS: # TODO-lev: perform a calculation based on this formula @@ -133,8 +134,8 @@ def binance( # Liquidation Price of USDⓈ-M Futures Contracts Cross # Isolated margin mode, then TMM=0,UPNL=0 - return (wb - tmm_1 + upnl_1 + cum_b - (side_1_both * position_1_both * ep1_both)) / ( - position_1_both * mmr_b - side_1_both * position_1_both) + return (wb - tmm_1 + upnl_1 + cum_b - side_1 * position * ep1) / ( + position * mmr_b - side_1 * position) # If nothing was returned exception("binance", trading_mode, collateral) diff --git a/tests/leverage/test_liquidation_price.py b/tests/leverage/test_liquidation_price.py index 687dd57f4..a3f014bb4 100644 --- a/tests/leverage/test_liquidation_price.py +++ b/tests/leverage/test_liquidation_price.py @@ -1,3 +1,5 @@ +from math import isclose + import pytest from freqtrade.enums import Collateral, TradingMode @@ -46,7 +48,14 @@ def test_liquidation_price_is_none( is_short, leverage, trading_mode, - collateral + collateral, + 1535443.01, + 71200.81144, + -56354.57, + 135365.00, + 3683.979, + 1456.84, + 0.10, ) is None @@ -80,34 +89,35 @@ def test_liquidation_price_exception_thrown( @pytest.mark.parametrize( - 'exchange_name,open_rate,is_short,leverage,trading_mode,collateral,result', [ - # Binance - ('binance', "2.0", False, "1.0", margin, cross, 1.0), - ('binance', "2.0", False, "1.0", futures, cross, 1.0), - ('binance', "2.0", False, "1.0", futures, isolated, 1.0), - # Kraken - ('kraken', "2.0", True, "3.0", margin, cross, 1.0), - ('kraken', "2.0", True, "3.0", futures, cross, 1.0), - # FTX - ('ftx', "2.0", False, "3.0", margin, cross, 1.0), - ('ftx', "2.0", False, "3.0", futures, cross, 1.0), - ] -) -def test_liquidation_price( - exchange_name, - open_rate, - is_short, - leverage, - trading_mode, - collateral, - result -): - # assert liquidation_price( - # exchange_name, - # open_rate, - # is_short, - # leverage, - # trading_mode, - # collateral - # ) == result - return # Here to avoid indent error + 'exchange_name, open_rate, is_short, leverage, trading_mode, collateral, wallet_balance, ' + 'mm_ex_1, upnl_ex_1, maintenance_amt, position, entry_price, ' + 'mm_rate, expected', + [ + ("binance", 0.0, False, 1, TradingMode.FUTURES, Collateral.ISOLATED, 1535443.01, 71200.8114, + -56354.57, 135365.00, 3683.979, 1456.84, 0.10, 1114.78), + ("binance", 0.0, False, 1, TradingMode.FUTURES, Collateral.ISOLATED, 1535443.01, 356512.508, + -448192.89, 16300.000, 109.488, 32481.980, 0.025, 18778.73), + ("binance", 0.0, False, 1, TradingMode.FUTURES, Collateral.CROSS, 1535443.01, 71200.81144, + -56354.57, 135365.00, 3683.979, 1456.84, 0.10, 1153.26), + ("binance", 0.0, False, 1, TradingMode.FUTURES, Collateral.CROSS, 1535443.01, 356512.508, + -448192.89, 16300.000, 109.488, 32481.980, 0.025, 26316.89) + ]) +def test_liquidation_price(exchange_name, open_rate, is_short, leverage, trading_mode, collateral, + wallet_balance, mm_ex_1, upnl_ex_1, + maintenance_amt, position, entry_price, mm_rate, + expected): + assert isclose(round(liquidation_price( + exchange_name=exchange_name, + 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, + maintenance_amt=maintenance_amt, + position=position, + entry_price=entry_price, + mm_rate=mm_rate + ), 2), expected) From d26a068adff0bc009514d2fd5e201ce28e52d9cc Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sun, 19 Sep 2021 00:51:06 -0600 Subject: [PATCH 10/53] rearanged isolated_liq in models a bit --- freqtrade/persistence/models.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index cd22d9ead..e23fd281c 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -375,7 +375,14 @@ class LocalTrade(): is_short=self.is_short, leverage=self.leverage, trading_mode=self.trading_mode, - collateral=Collateral.ISOLATED + collateral=Collateral.ISOLATED, + mm_ex_1=0.0, + upnl_ex_1=0.0, + # TODO-lev: position=amount * current_price, + # TODO-lev: maintenance_amt, + # TODO-lev: wallet_balance, + # TODO-lev: mm_rate, + ) if isolated_liq is None: raise OperationalException( From 447312d4c8416dea7c66ac40c5538e60f3948606 Mon Sep 17 00:00:00 2001 From: Arunavo Ray Date: Sun, 19 Sep 2021 12:36:01 +0530 Subject: [PATCH 11/53] Fixed parameter check which failed when 0.0 was passed --- freqtrade/leverage/liquidation_price.py | 12 ++++++++---- tests/leverage/test_liquidation_price.py | 8 ++++---- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/freqtrade/leverage/liquidation_price.py b/freqtrade/leverage/liquidation_price.py index a18649812..944789494 100644 --- a/freqtrade/leverage/liquidation_price.py +++ b/freqtrade/leverage/liquidation_price.py @@ -19,7 +19,6 @@ def liquidation_price( entry_price: Optional[float], mm_rate: Optional[float] ) -> Optional[float]: - if trading_mode == TradingMode.SPOT: return None @@ -30,9 +29,8 @@ def liquidation_price( ) if exchange_name.lower() == "binance": - if not wallet_balance or not mm_ex_1 or not upnl_ex_1 \ - or not maintenance_amt or not position or not entry_price \ - or not mm_rate: + if None in [wallet_balance, mm_ex_1, upnl_ex_1, maintenance_amt, position, entry_price, + mm_rate]: raise OperationalException( f"Parameters wallet_balance, mm_ex_1, upnl_ex_1, " f"maintenance_amt, position, entry_price, mm_rate " @@ -187,3 +185,9 @@ def ftx( # If nothing was returned exception("ftx", trading_mode, collateral) + + +if __name__ == '__main__': + print(liquidation_price("binance", 0.0, False, 1, TradingMode.FUTURES, Collateral.ISOLATED, + 1535443.01, 356512.508, + 0.0, 16300.000, 109.488, 32481.980, 0.025)) diff --git a/tests/leverage/test_liquidation_price.py b/tests/leverage/test_liquidation_price.py index a3f014bb4..e1fba4c4f 100644 --- a/tests/leverage/test_liquidation_price.py +++ b/tests/leverage/test_liquidation_price.py @@ -93,10 +93,10 @@ def test_liquidation_price_exception_thrown( 'mm_ex_1, upnl_ex_1, maintenance_amt, position, entry_price, ' 'mm_rate, expected', [ - ("binance", 0.0, False, 1, TradingMode.FUTURES, Collateral.ISOLATED, 1535443.01, 71200.8114, - -56354.57, 135365.00, 3683.979, 1456.84, 0.10, 1114.78), - ("binance", 0.0, False, 1, TradingMode.FUTURES, Collateral.ISOLATED, 1535443.01, 356512.508, - -448192.89, 16300.000, 109.488, 32481.980, 0.025, 18778.73), + ("binance", 0.0, False, 1, TradingMode.FUTURES, Collateral.ISOLATED, 1535443.01, 0.0, + 0.0, 135365.00, 3683.979, 1456.84, 0.10, 1114.78), + ("binance", 0.0, False, 1, TradingMode.FUTURES, Collateral.ISOLATED, 1535443.01, 0.0, + 0.0, 16300.000, 109.488, 32481.980, 0.025, 18778.73), ("binance", 0.0, False, 1, TradingMode.FUTURES, Collateral.CROSS, 1535443.01, 71200.81144, -56354.57, 135365.00, 3683.979, 1456.84, 0.10, 1153.26), ("binance", 0.0, False, 1, TradingMode.FUTURES, Collateral.CROSS, 1535443.01, 356512.508, From 92c94bb62acc9b8bc011695b7b51f255e6025f41 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Mon, 20 Sep 2021 21:44:00 -0600 Subject: [PATCH 12/53] added position and wallet_balance to LocalTrade.set_isolated_liq --- freqtrade/persistence/models.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index e23fd281c..f3b68a924 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -363,12 +363,22 @@ class LocalTrade(): self.stop_loss_pct = -1 * abs(percent) self.stoploss_last_update = datetime.utcnow() - def set_isolated_liq(self, isolated_liq: Optional[float]): + def set_isolated_liq( + self, + isolated_liq: Optional[float], + wallet_balance: Optional[float], + current_price: Optional[float] + ): """ Method you should use to set self.liquidation price. Assures stop_loss is not passed the liquidation price """ if not isolated_liq: + if not wallet_balance or not current_price: + raise OperationalException( + "wallet balance must be passed to LocalTrade.set_isolated_liq when param" + "isolated_liq is None" + ) isolated_liq = liquidation_price( exchange_name=self.exchange, open_rate=self.open_rate, @@ -378,9 +388,9 @@ class LocalTrade(): collateral=Collateral.ISOLATED, mm_ex_1=0.0, upnl_ex_1=0.0, - # TODO-lev: position=amount * current_price, + position=self.amount * current_price, + wallet_balance=wallet_balance, # TODO-lev: maintenance_amt, - # TODO-lev: wallet_balance, # TODO-lev: mm_rate, ) From 778e3bcba61339a93e64050e077141f4b020ace5 Mon Sep 17 00:00:00 2001 From: Arunavo Ray Date: Thu, 23 Sep 2021 11:54:05 +0530 Subject: [PATCH 13/53] Suppress incompatible type "Optional[float]"; expected "float" as the check exists. --- freqtrade/leverage/liquidation_price.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/freqtrade/leverage/liquidation_price.py b/freqtrade/leverage/liquidation_price.py index 944789494..7a796773b 100644 --- a/freqtrade/leverage/liquidation_price.py +++ b/freqtrade/leverage/liquidation_price.py @@ -36,9 +36,10 @@ def liquidation_price( f"maintenance_amt, position, entry_price, mm_rate " f"is required by liquidation_price when exchange is {exchange_name.lower()}") - return binance(open_rate, is_short, leverage, trading_mode, collateral, wallet_balance, - mm_ex_1, upnl_ex_1, maintenance_amt, - position, entry_price, mm_rate) + # Suppress incompatible type "Optional[float]"; expected "float" as the check exists above. + return binance(open_rate, is_short, leverage, trading_mode, collateral, # type: ignore + wallet_balance, mm_ex_1, upnl_ex_1, maintenance_amt, # type: ignore + position, entry_price, mm_rate) # type: ignore elif exchange_name.lower() == "kraken": return kraken(open_rate, is_short, leverage, trading_mode, collateral) elif exchange_name.lower() == "ftx": From eee7271ab8f65bf3a9aa03df314f858aa286c141 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Fri, 5 Nov 2021 14:25:22 -0600 Subject: [PATCH 14/53] Added live isolated-liq get --- freqtrade/exchange/exchange.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 58321a2db..ac3edad89 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1983,6 +1983,26 @@ class Exchange: else: return 0.0 + @retrier + def get_liquidation_price(self, pair: str): + ''' + Set's the margin mode on the exchange to cross or isolated for a specific pair + :param symbol: base/quote currency pair (e.g. "ADA/USDT") + ''' + if self._config['dry_run'] or not self.exchange_has("fetchPositions"): + # Some exchanges only support one collateral type + return + + try: + return self._api.fetch_positions(pair).liquidationPrice + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e + except (ccxt.NetworkError, ccxt.ExchangeError) as e: + raise TemporaryError( + f'Could not set margin mode due to {e.__class__.__name__}. Message: {e}') from e + except ccxt.BaseError as e: + raise OperationalException(e) from e + def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = None) -> bool: return exchange_name in ccxt_exchanges(ccxt_module) From ba02605d770d6f7aef5810a712285d3d3ac6f916 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Fri, 5 Nov 2021 19:27:06 -0600 Subject: [PATCH 15/53] Isolated liq branch passes all tests and has the general structure that it is supposed to, but is patchy, and doesnt get the correct maintenance amt and maintenance margin rate yet --- freqtrade/exchange/exchange.py | 10 ++++- freqtrade/freqtradebot.py | 31 +++++++++----- freqtrade/leverage/liquidation_price.py | 52 +++++++++++++++++------- freqtrade/persistence/models.py | 14 ++++--- tests/exchange/test_exchange.py | 18 ++++++++ tests/leverage/test_liquidation_price.py | 22 +++++----- 6 files changed, 103 insertions(+), 44 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index ac3edad89..07bc0ae61 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1987,7 +1987,7 @@ class Exchange: def get_liquidation_price(self, pair: str): ''' Set's the margin mode on the exchange to cross or isolated for a specific pair - :param symbol: base/quote currency pair (e.g. "ADA/USDT") + :param pair: base/quote currency pair (e.g. "ADA/USDT") ''' if self._config['dry_run'] or not self.exchange_has("fetchPositions"): # Some exchanges only support one collateral type @@ -2003,6 +2003,14 @@ class Exchange: except ccxt.BaseError as e: raise OperationalException(e) from e + @retrier + def get_mm_amt_rate(self, pair: str, amount: float): + ''' + :return: The maintenance amount, and maintenance margin rate + ''' + # TODO-lev: return the real amounts + return 0, 0.4 + def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = None) -> bool: return exchange_name in ccxt_exchanges(ccxt_module) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 113c78a15..93eb27bb4 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -21,6 +21,7 @@ from freqtrade.enums import (Collateral, RPCMessageType, RunMode, SellType, Sign from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError, InvalidOrderException, PricingError) from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds +from freqtrade.leverage import liquidation_price from freqtrade.misc import safe_value_fallback, safe_value_fallback2 from freqtrade.mixins import LoggingMixin from freqtrade.persistence import Order, PairLocks, Trade, cleanup_db, init_db @@ -609,24 +610,32 @@ class FreqtradeBot(LoggingMixin): interest_rate = 0.0 isolated_liq = None - # TODO-lev: Uncomment once liq and interest merged in # if TradingMode == TradingMode.MARGIN: # interest_rate = self.exchange.get_interest_rate( # pair=pair, # open_rate=open_rate, # is_short=is_short # ) + maintenance_amt, mm_rate = self.exchange.get_mm_amt_rate(pair, amount) - # if self.collateral_type == Collateral.ISOLATED: - - # isolated_liq = liquidation_price( - # exchange_name=self.exchange.name, - # trading_mode=self.trading_mode, - # open_rate=open_rate, - # amount=amount, - # leverage=leverage, - # is_short=is_short - # ) + if self.collateral_type == Collateral.ISOLATED: + if self.config['dry_run']: + isolated_liq = liquidation_price( + exchange_name=self.exchange.name, + 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, + position=amount * open_rate, + wallet_balance=amount/leverage, # TODO-lev: Is this correct? + maintenance_amt=maintenance_amt, + mm_rate=mm_rate, + ) + else: + isolated_liq = self.exchange.get_liquidation_price(pair) return interest_rate, isolated_liq diff --git a/freqtrade/leverage/liquidation_price.py b/freqtrade/leverage/liquidation_price.py index 7a796773b..2519bab52 100644 --- a/freqtrade/leverage/liquidation_price.py +++ b/freqtrade/leverage/liquidation_price.py @@ -16,7 +16,6 @@ def liquidation_price( upnl_ex_1: Optional[float], maintenance_amt: Optional[float], position: Optional[float], - entry_price: Optional[float], mm_rate: Optional[float] ) -> Optional[float]: if trading_mode == TradingMode.SPOT: @@ -29,17 +28,33 @@ def liquidation_price( ) if exchange_name.lower() == "binance": - if None in [wallet_balance, mm_ex_1, upnl_ex_1, maintenance_amt, position, entry_price, - mm_rate]: + if ( + wallet_balance is None or + mm_ex_1 is None or + upnl_ex_1 is None or + maintenance_amt is None or + position is None or + mm_rate is None + ): raise OperationalException( f"Parameters wallet_balance, mm_ex_1, upnl_ex_1, " - f"maintenance_amt, position, entry_price, mm_rate " + f"maintenance_amt, position, mm_rate " f"is required by liquidation_price when exchange is {exchange_name.lower()}") # Suppress incompatible type "Optional[float]"; expected "float" as the check exists above. - return binance(open_rate, is_short, leverage, trading_mode, collateral, # type: ignore - wallet_balance, mm_ex_1, upnl_ex_1, maintenance_amt, # type: ignore - position, entry_price, mm_rate) # type: ignore + return binance( + open_rate=open_rate, + is_short=is_short, + leverage=leverage, + trading_mode=trading_mode, + collateral=collateral, # type: ignore + wallet_balance=wallet_balance, + mm_ex_1=mm_ex_1, + upnl_ex_1=upnl_ex_1, + maintenance_amt=maintenance_amt, # type: ignore + position=position, + mm_rate=mm_rate, + ) # type: ignore elif exchange_name.lower() == "kraken": return kraken(open_rate, is_short, leverage, trading_mode, collateral) elif exchange_name.lower() == "ftx": @@ -76,12 +91,10 @@ def binance( upnl_ex_1: float, maintenance_amt: float, position: float, - entry_price: float, mm_rate: float, ): """ Calculates the liquidation price on Binance - :param open_rate: open_rate :param is_short: true or false :param leverage: leverage in float :param trading_mode: spot, margin, futures @@ -100,7 +113,7 @@ def binance( :param position: Absolute value of position size (one-way mode) - :param entry_price: Entry Price of position (one-way mode) + :param open_rate: Entry Price of position (one-way mode) :param mm_rate: Maintenance margin rate of position (one-way mode) @@ -112,7 +125,7 @@ def binance( cum_b = maintenance_amt side_1 = -1 if is_short else 1 position = abs(position) - ep1 = entry_price + ep1 = open_rate mmr_b = mm_rate if trading_mode == TradingMode.MARGIN and collateral == Collateral.CROSS: @@ -189,6 +202,17 @@ def ftx( if __name__ == '__main__': - print(liquidation_price("binance", 0.0, False, 1, TradingMode.FUTURES, Collateral.ISOLATED, - 1535443.01, 356512.508, - 0.0, 16300.000, 109.488, 32481.980, 0.025)) + print(liquidation_price( + "binance", + 32481.980, + False, + 1, + TradingMode.FUTURES, + Collateral.ISOLATED, + 1535443.01, + 356512.508, + 0.0, + 16300.000, + 109.488, + 0.025 + )) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index f3b68a924..abe6d0237 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -365,9 +365,11 @@ class LocalTrade(): def set_isolated_liq( self, - isolated_liq: Optional[float], - wallet_balance: Optional[float], - current_price: Optional[float] + isolated_liq: Optional[float] = None, + wallet_balance: Optional[float] = None, + current_price: Optional[float] = None, + maintenance_amt: Optional[float] = None, + mm_rate: Optional[float] = None, ): """ Method you should use to set self.liquidation price. @@ -389,9 +391,9 @@ class LocalTrade(): mm_ex_1=0.0, upnl_ex_1=0.0, position=self.amount * current_price, - wallet_balance=wallet_balance, - # TODO-lev: maintenance_amt, - # TODO-lev: mm_rate, + wallet_balance=self.amount / self.leverage, # TODO-lev: Is this correct? + maintenance_amt=maintenance_amt, + mm_rate=mm_rate, ) if isolated_liq is None: diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 5836aa4e4..0ac7a6130 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -3582,6 +3582,24 @@ def test_calculate_funding_fees( ) == kraken_fee +def test_get_liquidation_price(mocker, default_conf): + + api_mock = MagicMock() + api_mock.fetch_positions = MagicMock() + type(api_mock).has = PropertyMock(return_value={'fetchPositions': True}) + default_conf['dry_run'] = False + + ccxt_exceptionhandlers( + mocker, + default_conf, + api_mock, + "binance", + "get_liquidation_price", + "fetch_positions", + pair="XRP/USDT" + ) + + @pytest.mark.parametrize('exchange,rate_start,rate_end,d1,d2,amount,expected_fees', [ ('binance', 0, 2, "2021-09-01 00:00:00", "2021-09-01 08:00:00", 30.0, -0.0009140999999999999), ('binance', 0, 2, "2021-09-01 00:00:15", "2021-09-01 08:00:00", 30.0, -0.0009140999999999999), diff --git a/tests/leverage/test_liquidation_price.py b/tests/leverage/test_liquidation_price.py index e1fba4c4f..300c316d9 100644 --- a/tests/leverage/test_liquidation_price.py +++ b/tests/leverage/test_liquidation_price.py @@ -54,7 +54,6 @@ def test_liquidation_price_is_none( -56354.57, 135365.00, 3683.979, - 1456.84, 0.10, ) is None @@ -89,23 +88,23 @@ def test_liquidation_price_exception_thrown( @pytest.mark.parametrize( - 'exchange_name, open_rate, is_short, leverage, trading_mode, collateral, wallet_balance, ' - 'mm_ex_1, upnl_ex_1, maintenance_amt, position, entry_price, ' + 'exchange_name, is_short, leverage, trading_mode, collateral, wallet_balance, ' + 'mm_ex_1, upnl_ex_1, maintenance_amt, position, open_rate, ' 'mm_rate, expected', [ - ("binance", 0.0, 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), - ("binance", 0.0, False, 1, TradingMode.FUTURES, Collateral.ISOLATED, 1535443.01, 0.0, + ("binance", False, 1, TradingMode.FUTURES, Collateral.ISOLATED, 1535443.01, 0.0, 0.0, 16300.000, 109.488, 32481.980, 0.025, 18778.73), - ("binance", 0.0, False, 1, TradingMode.FUTURES, Collateral.CROSS, 1535443.01, 71200.81144, + ("binance", False, 1, TradingMode.FUTURES, Collateral.CROSS, 1535443.01, 71200.81144, -56354.57, 135365.00, 3683.979, 1456.84, 0.10, 1153.26), - ("binance", 0.0, False, 1, TradingMode.FUTURES, Collateral.CROSS, 1535443.01, 356512.508, + ("binance", False, 1, TradingMode.FUTURES, Collateral.CROSS, 1535443.01, 356512.508, -448192.89, 16300.000, 109.488, 32481.980, 0.025, 26316.89) ]) -def test_liquidation_price(exchange_name, open_rate, is_short, leverage, trading_mode, collateral, - wallet_balance, mm_ex_1, upnl_ex_1, - maintenance_amt, position, entry_price, mm_rate, - expected): +def test_liquidation_price( + exchange_name, open_rate, is_short, leverage, trading_mode, collateral, wallet_balance, + mm_ex_1, upnl_ex_1, maintenance_amt, position, mm_rate, expected +): assert isclose(round(liquidation_price( exchange_name=exchange_name, open_rate=open_rate, @@ -118,6 +117,5 @@ def test_liquidation_price(exchange_name, open_rate, is_short, leverage, trading upnl_ex_1=upnl_ex_1, maintenance_amt=maintenance_amt, position=position, - entry_price=entry_price, mm_rate=mm_rate ), 2), expected) From 5796d95a953560410f4d0d8cc277e5b088a5ea5e Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sat, 13 Nov 2021 21:12:36 -0600 Subject: [PATCH 16/53] Added gateio and okex isolated liquidation formulas --- freqtrade/leverage/liquidation_price.py | 282 +++++++++++++++++++----- freqtrade/persistence/models.py | 3 +- 2 files changed, 228 insertions(+), 57 deletions(-) diff --git a/freqtrade/leverage/liquidation_price.py b/freqtrade/leverage/liquidation_price.py index 2519bab52..5a14716e1 100644 --- a/freqtrade/leverage/liquidation_price.py +++ b/freqtrade/leverage/liquidation_price.py @@ -6,18 +6,74 @@ from freqtrade.exceptions import OperationalException def liquidation_price( exchange_name: str, - open_rate: float, + open_rate: float, # (b) Entry price of position is_short: bool, leverage: float, trading_mode: TradingMode, collateral: Optional[Collateral], - wallet_balance: Optional[float], - mm_ex_1: Optional[float], - upnl_ex_1: Optional[float], - maintenance_amt: Optional[float], - position: Optional[float], - mm_rate: Optional[float] + # Binance + collateral_amount: Optional[float] = None, # (bg) + mm_ex_1: Optional[float] = None, # (b) + upnl_ex_1: Optional[float] = None, # (b) + maintenance_amt: Optional[float] = None, # (b) (cum_b) + position: Optional[float] = None, # (b) Absolute value of position size + mm_rate: Optional[float] = None, # (b) + # Gateio & Okex + mm_ratio: Optional[float] = None, # (go) + taker_fee_rate: Optional[float] = None, # (go) + # Gateio + base_size: Optional[float] = None, # (g) + # Okex + liability: Optional[float] = None, # (o) + interest: Optional[float] = None, # (o) + position_assets: Optional[float] = None, # (o) ) -> Optional[float]: + ''' + exchange_name + is_short + leverage + trading_mode + collateral + # + open_rate - b + collateral_amount - bg + In Cross margin mode, WB is crossWalletBalance + 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. + Under the cross margin mode, the same ticker/symbol, + 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 + on the margin allocated to the positions. + mm_ex_1 - b + Maintenance Margin of all other contracts, excluding Contract 1 + If it is an isolated margin mode, then TMM=0,UPNL=0 + upnl_ex_1 - b + Unrealized PNL of all other contracts, excluding Contract 1 + If it is an isolated margin mode, then UPNL=0 + maintenance_amt (cumb) - b + Maintenance Amount of position + position - b + Absolute value of position size + mm_rate - b + Maintenance margin rate of position + # Gateio & okex + mm_ratio - go + - [assets in the position - (liability +interest) * mark price] / (maintenance margin + liquidation fee) (okex) + taker_fee_rate - go + # Gateio + base_size - g + The size of the position in base currency + # Okex + liability - o + Initial liabilities + deducted interest + • Long positions: Liability is calculated in quote currency. + • Short positions: Liability is calculated in trading currency. + interest - o + Interest that has not been deducted yet. + position_assets - o + # * I think this is the same as collateral_amount + Total position assets – on-hold by pending order + ''' if trading_mode == TradingMode.SPOT: return None @@ -29,7 +85,7 @@ def liquidation_price( if exchange_name.lower() == "binance": if ( - wallet_balance is None or + collateral_amount is None or mm_ex_1 is None or upnl_ex_1 is None or maintenance_amt is None or @@ -48,7 +104,7 @@ def liquidation_price( leverage=leverage, trading_mode=trading_mode, collateral=collateral, # type: ignore - wallet_balance=wallet_balance, + wallet_balance=collateral_amount, mm_ex_1=mm_ex_1, upnl_ex_1=upnl_ex_1, maintenance_amt=maintenance_amt, # type: ignore @@ -59,6 +115,51 @@ def liquidation_price( return kraken(open_rate, is_short, leverage, trading_mode, collateral) elif exchange_name.lower() == "ftx": return ftx(open_rate, is_short, leverage, trading_mode, collateral) + elif exchange_name.lower() == "gateio": + if ( + not collateral_amount or + not base_size or + not mm_ratio or + not taker_fee_rate + ): + raise OperationalException( + f"{exchange_name} {collateral} {trading_mode} requires parameters " + f"collateral_amount, contract_size, num_contracts, mm_ratio and taker_fee" + ) + else: + return gateio( + open_rate=open_rate, + is_short=is_short, + trading_mode=trading_mode, + collateral=collateral, + collateral_amount=collateral_amount, + base_size=base_size, + mm_ratio=mm_ratio, + taker_fee_rate=taker_fee_rate + ) + elif exchange_name.lower() == "okex": + if ( + not mm_ratio or + not liability or + not interest or + not taker_fee_rate or + not position_assets + ): + raise OperationalException( + f"{exchange_name} {collateral} {trading_mode} requires parameters " + f"mm_ratio, liability, interest, taker_fee_rate, position_assets" + ) + else: + return okex( + is_short=is_short, + trading_mode=trading_mode, + collateral=collateral, + mm_ratio=mm_ratio, + liability=liability, + interest=interest, + taker_fee_rate=taker_fee_rate, + position_assets=position_assets, + ) raise OperationalException( f"liquidation_price is not implemented for {exchange_name}" ) @@ -94,29 +195,21 @@ def binance( mm_rate: float, ): """ - Calculates the liquidation price on Binance - :param is_short: true or false - :param leverage: leverage in float - :param trading_mode: spot, margin, futures - :param collateral: cross, isolated - - :param wallet_balance: Wallet Balance is crossWalletBalance in Cross-Margin Mode. - Wallet Balance is isolatedWalletBalance in Isolated Margin Mode - - :param mm_ex_1: Maintenance Margin of all other contracts, - excluding Contract 1. If it is an isolated margin mode, then TMM=0 - - :param upnl_ex_1: Unrealized PNL of all other contracts, excluding Contract 1. - If it is an isolated margin mode, then UPNL=0 - - :param maintenance_amt: Maintenance Amount of position (one-way mode) - - :param position: Absolute value of position size (one-way mode) - - :param open_rate: Entry Price of position (one-way mode) - - :param mm_rate: Maintenance margin rate of position (one-way mode) - + Calculates the liquidation price on Binance + :param is_short: true or false + :param leverage: leverage in float + :param trading_mode: spot, margin, futures + :param collateral: cross, isolated + :param wallet_balance: Wallet Balance is crossWalletBalance in Cross-Margin Mode. + Wallet Balance is isolatedWalletBalance in Isolated Margin Mode + :param mm_ex_1: Maintenance Margin of all other contracts, + excluding Contract 1. If it is an isolated margin mode, then TMM=0 + :param upnl_ex_1: Unrealized PNL of all other contracts, excluding Contract 1. + If it is an isolated margin mode, then UPNL=0 + :param maintenance_amt: Maintenance Amount of position (one-way mode) + :param position: Absolute value of position size (one-way mode) + :param open_rate: Entry Price of position (one-way mode) + :param mm_rate: Maintenance margin rate of position (one-way mode) """ # TODO-lev: Additional arguments, fill in formulas wb = wallet_balance @@ -161,10 +254,9 @@ def kraken( collateral: Collateral ): """ - Calculates the liquidation price on Kraken - :param name: Name of the exchange - :param trading_mode: spot, margin, futures - :param collateral: cross, isolated + Calculates the liquidation price on Kraken + :param trading_mode: spot, margin, futures + :param collateral: cross, isolated """ # TODO-lev: Additional arguments, fill in formulas @@ -188,10 +280,9 @@ def ftx( collateral: Collateral ): """ - Calculates the liquidation price on FTX - :param name: Name of the exchange - :param trading_mode: spot, margin, futures - :param collateral: cross, isolated + Calculates the liquidation price on FTX + :param trading_mode: spot, margin, futures + :param collateral: cross, isolated """ if collateral == Collateral.CROSS: # TODO-lev: Additional arguments, fill in formulas @@ -201,18 +292,99 @@ def ftx( exception("ftx", trading_mode, collateral) -if __name__ == '__main__': - print(liquidation_price( - "binance", - 32481.980, - False, - 1, - TradingMode.FUTURES, - Collateral.ISOLATED, - 1535443.01, - 356512.508, - 0.0, - 16300.000, - 109.488, - 0.025 - )) +def gateio( + open_rate: float, + is_short: bool, + trading_mode: TradingMode, + collateral: Collateral, + collateral_amount: float, + base_size: float, + mm_ratio: float, + taker_fee_rate: float, + is_inverse: bool = False +): + """ + Calculates the liquidation price on Gate.io + :param open_rate: Entry Price of position + :param is_short: True for short trades + :param trading_mode: spot, margin, futures + :param collateral: cross, isolated + :param collateral_amount: Also called margin + :param base_size: size of position in base currency + contract_size / num_contracts + contract_size: How much one contract is worth + num_contracts: Also called position + :param mm_ratio: Viewed in contract details + :param taker_fee_rate: + :param is_inverse: True if settle currency matches base currency + + ( Opening Price ± Margin/Contract Multiplier/Position ) / [ 1 ± ( MMR + Taker Fee)] + '±' in the formula refers to the direction of the contract, + go long refers to '-' + go short refers to '+' + Position refers to the number of contracts. + Maintenance Margin Ratio and Contract Multiplier can be viewed in the Contract Details. + + https://www.gate.io/help/futures/perpetual/22160/calculation-of-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 = collateral_amount / base_size + + 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) + else: + exception("gatio", trading_mode, collateral) + + +def okex( + is_short: bool, + trading_mode: TradingMode, + collateral: Collateral, + liability: float, + interest: float, + mm_ratio: float, + taker_fee_rate: float, + position_assets: float +): + ''' + https://www.okex.com/support/hc/en-us/articles/ + 360053909592-VI-Introduction-to-the-isolated-mode-of-Single-Multi-currency-Portfolio-margin + + Initial liabilities + deducted interest + Long positions: Liability is calculated in quote currency. + Short positions: Liability is calculated in trading currency. + interest: Interest that has not been deducted yet. + Margin ratio + Long: [position_assets - (liability + interest) / mark_price] / (maintenance_margin + fees) + Short: [position_assets - (liability + interest) * mark_price] / (maintenance_margin + fees) + ''' + if trading_mode == TradingMode.FUTURES and collateral == Collateral.ISOLATED: + if is_short: + return (liability + interest) * (1 + mm_ratio) * (1 + taker_fee_rate) + else: + return (liability + interest) * (1 + mm_ratio) * (1 + taker_fee_rate) / position_assets + else: + exception("okex", trading_mode, collateral) + +# if __name__ == '__main__': +# print(liquidation_price( +# "binance", +# 32481.980, +# False, +# 1, +# TradingMode.FUTURES, +# Collateral.ISOLATED, +# 1535443.01, +# 356512.508, +# 0.0, +# 16300.000, +# 109.488, +# 0.025 +# )) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index abe6d0237..91f458cb6 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -16,8 +16,7 @@ from sqlalchemy.sql.schema import UniqueConstraint from freqtrade.constants import DATETIME_PRINT_FORMAT, NON_OPEN_EXCHANGE_STATES from freqtrade.enums import Collateral, SellType, TradingMode from freqtrade.exceptions import DependencyException, OperationalException -from freqtrade.leverage import interest -from freqtrade.leverage import liquidation_price +from freqtrade.leverage import interest, liquidation_price from freqtrade.misc import safe_value_fallback from freqtrade.persistence.migrations import check_migrate From 42360592ba6512aa018e29925e20f431b7baf1d5 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sat, 8 Jan 2022 02:25:17 -0600 Subject: [PATCH 17/53] trimmed down liquidation_price variable and edited comments --- freqtrade/leverage/liquidation_price.py | 135 ++++++++++++------------ 1 file changed, 68 insertions(+), 67 deletions(-) diff --git a/freqtrade/leverage/liquidation_price.py b/freqtrade/leverage/liquidation_price.py index 5a14716e1..30b8885ba 100644 --- a/freqtrade/leverage/liquidation_price.py +++ b/freqtrade/leverage/liquidation_price.py @@ -6,73 +6,74 @@ from freqtrade.exceptions import OperationalException def liquidation_price( exchange_name: str, - open_rate: float, # (b) Entry price of position + open_rate: float, # Entry price of position is_short: bool, leverage: float, trading_mode: TradingMode, - collateral: Optional[Collateral], + mm_ratio: float, + collateral: Optional[Collateral] = Collateral.ISOLATED, + # Binance - collateral_amount: Optional[float] = None, # (bg) - mm_ex_1: Optional[float] = None, # (b) - upnl_ex_1: Optional[float] = None, # (b) - maintenance_amt: Optional[float] = None, # (b) (cum_b) - position: Optional[float] = None, # (b) Absolute value of position size - mm_rate: Optional[float] = None, # (b) + maintenance_amt: Optional[float] = None, + + # Binance and Gateio + wallet_balance: Optional[float] = None, + position: Optional[float] = None, # Absolute value of position size + # Gateio & Okex - mm_ratio: Optional[float] = None, # (go) - taker_fee_rate: Optional[float] = None, # (go) - # Gateio - base_size: Optional[float] = None, # (g) + taker_fee_rate: Optional[float] = None, + # Okex - liability: Optional[float] = None, # (o) - interest: Optional[float] = None, # (o) - position_assets: Optional[float] = None, # (o) + liability: Optional[float] = None, + interest: Optional[float] = None, + position_assets: Optional[float] = None, # * Might be same as position + + # * Cross only + mm_ex_1: Optional[float] = 0.0, # Cross only + upnl_ex_1: Optional[float] = 0.0, # Cross only ) -> Optional[float]: ''' - exchange_name - is_short - leverage - trading_mode - collateral - # - open_rate - b - collateral_amount - bg + wallet_balance In Cross margin mode, WB is crossWalletBalance 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. - 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. Under the isolated mode, each isolated position will have different liquidation prices depending on the margin allocated to the positions. - mm_ex_1 - b - Maintenance Margin of all other contracts, excluding Contract 1 - If it is an isolated margin mode, then TMM=0,UPNL=0 - upnl_ex_1 - b - Unrealized PNL of all other contracts, excluding Contract 1 - If it is an isolated margin mode, then UPNL=0 - maintenance_amt (cumb) - b + position + Absolute value of position size (in base currency) + + # Binance + maintenance_amt (cumb) Maintenance Amount of position - position - b - Absolute value of position size - mm_rate - b - Maintenance margin rate of position + + # Gateio & okex & binance + mm_ratio + [assets in the position - (liability +interest) * mark price] / + (maintenance margin + liquidation fee) (okex) + # * Note: Binance's formula specifies maintenance margin rate which is mm_ratio * 100% + # Gateio & okex - mm_ratio - go - - [assets in the position - (liability +interest) * mark price] / (maintenance margin + liquidation fee) (okex) - taker_fee_rate - go - # Gateio - base_size - g - The size of the position in base currency + taker_fee_rate + # Okex - liability - o + liability Initial liabilities + deducted interest • Long positions: Liability is calculated in quote currency. • Short positions: Liability is calculated in trading currency. - interest - o + interest Interest that has not been deducted yet. - position_assets - o - # * I think this is the same as collateral_amount + position_assets Total position assets – on-hold by pending order + + # * Cross only + mm_ex_1 + Maintenance Margin of all other contracts, excluding Contract 1 + If it is an isolated margin mode, then TMM=0,UPNL=0 + upnl_ex_1 + Unrealized PNL of all other contracts, excluding Contract 1 + If it is an isolated margin mode, then UPNL=0 ''' if trading_mode == TradingMode.SPOT: return None @@ -85,16 +86,16 @@ def liquidation_price( if exchange_name.lower() == "binance": if ( - collateral_amount is None or - mm_ex_1 is None or - upnl_ex_1 is None or + wallet_balance is None or + # mm_ex_1 is None or # * Cross only + # upnl_ex_1 is None or # * Cross only maintenance_amt is None or position is None or - mm_rate is None + mm_ratio is None ): raise OperationalException( f"Parameters wallet_balance, mm_ex_1, upnl_ex_1, " - f"maintenance_amt, position, mm_rate " + f"maintenance_amt, position, mm_ratio " f"is required by liquidation_price when exchange is {exchange_name.lower()}") # Suppress incompatible type "Optional[float]"; expected "float" as the check exists above. @@ -104,12 +105,12 @@ def liquidation_price( leverage=leverage, trading_mode=trading_mode, collateral=collateral, # type: ignore - wallet_balance=collateral_amount, - mm_ex_1=mm_ex_1, - upnl_ex_1=upnl_ex_1, + wallet_balance=wallet_balance, + # mm_ex_1=mm_ex_1, + # upnl_ex_1=upnl_ex_1, maintenance_amt=maintenance_amt, # type: ignore position=position, - mm_rate=mm_rate, + mm_ratio=mm_ratio, ) # type: ignore elif exchange_name.lower() == "kraken": return kraken(open_rate, is_short, leverage, trading_mode, collateral) @@ -117,14 +118,14 @@ def liquidation_price( return ftx(open_rate, is_short, leverage, trading_mode, collateral) elif exchange_name.lower() == "gateio": if ( - not collateral_amount or - not base_size or + not wallet_balance or + not position or not mm_ratio or not taker_fee_rate ): raise OperationalException( f"{exchange_name} {collateral} {trading_mode} requires parameters " - f"collateral_amount, contract_size, num_contracts, mm_ratio and taker_fee" + f"wallet_balance, contract_size, num_contracts, mm_ratio and taker_fee" ) else: return gateio( @@ -132,8 +133,8 @@ def liquidation_price( is_short=is_short, trading_mode=trading_mode, collateral=collateral, - collateral_amount=collateral_amount, - base_size=base_size, + wallet_balance=wallet_balance, + position=position, mm_ratio=mm_ratio, taker_fee_rate=taker_fee_rate ) @@ -192,7 +193,7 @@ def binance( upnl_ex_1: float, maintenance_amt: float, position: float, - mm_rate: float, + mm_ratio: float, ): """ Calculates the liquidation price on Binance @@ -209,7 +210,7 @@ def binance( :param maintenance_amt: Maintenance Amount of position (one-way mode) :param position: Absolute value of position size (one-way mode) :param open_rate: Entry Price of position (one-way mode) - :param mm_rate: Maintenance margin rate of position (one-way mode) + :param mm_ratio: Maintenance margin rate of position (one-way mode) """ # TODO-lev: Additional arguments, fill in formulas wb = wallet_balance @@ -219,7 +220,7 @@ def binance( side_1 = -1 if is_short else 1 position = abs(position) ep1 = open_rate - mmr_b = mm_rate + mmr_b = mm_ratio if trading_mode == TradingMode.MARGIN and collateral == Collateral.CROSS: # TODO-lev: perform a calculation based on this formula @@ -297,8 +298,8 @@ def gateio( is_short: bool, trading_mode: TradingMode, collateral: Collateral, - collateral_amount: float, - base_size: float, + wallet_balance: float, + position: float, mm_ratio: float, taker_fee_rate: float, is_inverse: bool = False @@ -309,8 +310,8 @@ def gateio( :param is_short: True for short trades :param trading_mode: spot, margin, futures :param collateral: cross, isolated - :param collateral_amount: Also called margin - :param base_size: size of position in base currency + :param wallet_balance: Also called margin + :param position: size of position in base currency contract_size / num_contracts contract_size: How much one contract is worth num_contracts: Also called position @@ -332,7 +333,7 @@ def gateio( if is_inverse: raise OperationalException( "Freqtrade does not support inverse contracts at the moment") - value = collateral_amount / base_size + value = wallet_balance / position mm_ratio_taker = (mm_ratio + taker_fee_rate) if is_short: From e0df7ee72abeb2a48d7cbbdb2aba7d705118f265 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Mon, 10 Jan 2022 01:40:40 -0600 Subject: [PATCH 18/53] Changed variable names in binance.get_max_leverage --- freqtrade/exchange/binance.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 6fcead08c..30ec3288f 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -165,9 +165,9 @@ class Binance(Exchange): return 1.0 pair_brackets = self._leverage_brackets[pair] max_lev = 1.0 - for [min_amount, margin_req] in pair_brackets: - if nominal_value >= min_amount: - max_lev = 1/margin_req + for [notional_floor, maint_margin_ratio] in pair_brackets: + if nominal_value >= notional_floor: + max_lev = 1/maint_margin_ratio return max_lev @retrier From ba5fc21d84a23cde5ebf15a0719e1334df511a5b Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Mon, 10 Jan 2022 06:45:54 -0600 Subject: [PATCH 19/53] added isolated futures to supported modes for binance,gateio --- freqtrade/exchange/binance.py | 3 +-- freqtrade/exchange/ftx.py | 1 + freqtrade/exchange/gateio.py | 2 +- tests/exchange/test_exchange.py | 6 ++---- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 30ec3288f..149d824b6 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -33,10 +33,9 @@ class Binance(Exchange): _supported_trading_mode_collateral_pairs: List[Tuple[TradingMode, Collateral]] = [ # TradingMode.SPOT always supported and not required in this list - # TODO-lev: Uncomment once supported # (TradingMode.MARGIN, Collateral.CROSS), # (TradingMode.FUTURES, Collateral.CROSS), - # (TradingMode.FUTURES, Collateral.ISOLATED) + (TradingMode.FUTURES, Collateral.ISOLATED) ] def stoploss_adjust(self, stop_loss: float, order: Dict, side: str) -> bool: diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index e28c6bc45..e22281faf 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -27,6 +27,7 @@ class Ftx(Exchange): _supported_trading_mode_collateral_pairs: List[Tuple[TradingMode, Collateral]] = [ # TradingMode.SPOT always supported and not required in this list + # TODO-lev: Uncomment once supported # (TradingMode.MARGIN, Collateral.CROSS), # (TradingMode.FUTURES, Collateral.CROSS) ] diff --git a/freqtrade/exchange/gateio.py b/freqtrade/exchange/gateio.py index fe4227d80..203aa735b 100644 --- a/freqtrade/exchange/gateio.py +++ b/freqtrade/exchange/gateio.py @@ -31,7 +31,7 @@ class Gateio(Exchange): # TradingMode.SPOT always supported and not required in this list # (TradingMode.MARGIN, Collateral.CROSS), # (TradingMode.FUTURES, Collateral.CROSS), - # (TradingMode.FUTURES, Collateral.ISOLATED) + (TradingMode.FUTURES, Collateral.ISOLATED) ] def validate_ordertypes(self, order_types: Dict) -> None: diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 0ac7a6130..b529af47f 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -3442,26 +3442,24 @@ def test_set_margin_mode(mocker, default_conf, collateral): # TODO-lev: Remove once implemented ("binance", TradingMode.MARGIN, Collateral.CROSS, True), ("binance", TradingMode.FUTURES, Collateral.CROSS, True), - ("binance", TradingMode.FUTURES, Collateral.ISOLATED, True), ("kraken", TradingMode.MARGIN, Collateral.CROSS, True), ("kraken", TradingMode.FUTURES, Collateral.CROSS, True), ("ftx", TradingMode.MARGIN, Collateral.CROSS, True), ("ftx", TradingMode.FUTURES, Collateral.CROSS, True), ("gateio", TradingMode.MARGIN, Collateral.CROSS, True), ("gateio", TradingMode.FUTURES, Collateral.CROSS, True), - ("gateio", TradingMode.FUTURES, Collateral.ISOLATED, True), # TODO-lev: Uncomment once implemented # ("binance", TradingMode.MARGIN, Collateral.CROSS, False), # ("binance", TradingMode.FUTURES, Collateral.CROSS, False), - # ("binance", TradingMode.FUTURES, Collateral.ISOLATED, False), + ("binance", TradingMode.FUTURES, Collateral.ISOLATED, False), # ("kraken", TradingMode.MARGIN, Collateral.CROSS, False), # ("kraken", TradingMode.FUTURES, Collateral.CROSS, False), # ("ftx", TradingMode.MARGIN, Collateral.CROSS, False), # ("ftx", TradingMode.FUTURES, Collateral.CROSS, False), # ("gateio", TradingMode.MARGIN, Collateral.CROSS, False), # ("gateio", TradingMode.FUTURES, Collateral.CROSS, False), - # ("gateio", TradingMode.FUTURES, Collateral.ISOLATED, False), + ("gateio", TradingMode.FUTURES, Collateral.ISOLATED, False), ]) def test_validate_trading_mode_and_collateral( default_conf, From 69a6223ca008fe0cd207d35ab77e59730f04e540 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Mon, 10 Jan 2022 07:17:17 -0600 Subject: [PATCH 20/53] implemented binance.get_maintenance_ratio_and_amt --- freqtrade/exchange/binance.py | 118 ++++++++++++++++++- freqtrade/exchange/exchange.py | 7 +- freqtrade/freqtradebot.py | 6 +- freqtrade/leverage/liquidation_price.py | 8 +- freqtrade/persistence/models.py | 16 ++- tests/exchange/test_binance.py | 140 +++++++++++++++-------- tests/leverage/test_liquidation_price.py | 6 +- 7 files changed, 236 insertions(+), 65 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 149d824b6..436c6f75e 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -163,11 +163,10 @@ class Binance(Exchange): if pair not in self._leverage_brackets: return 1.0 pair_brackets = self._leverage_brackets[pair] - max_lev = 1.0 - for [notional_floor, maint_margin_ratio] in pair_brackets: + for [notional_floor, mm_ratio, _] in reversed(pair_brackets): if nominal_value >= notional_floor: - max_lev = 1/maint_margin_ratio - return max_lev + return 1/mm_ratio + return 1.0 @retrier def _set_leverage( @@ -227,3 +226,114 @@ class Binance(Exchange): :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) + + 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 ") diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 07bc0ae61..38f3a0b99 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -2003,8 +2003,11 @@ class Exchange: except ccxt.BaseError as e: raise OperationalException(e) from e - @retrier - def get_mm_amt_rate(self, pair: str, amount: float): + def get_maintenance_ratio_and_amt( + self, + pair: Optional[str], + nominal_value: Optional[float] + ): ''' :return: The maintenance amount, and maintenance margin rate ''' diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 93eb27bb4..80656f209 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -616,7 +616,7 @@ class FreqtradeBot(LoggingMixin): # open_rate=open_rate, # 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.config['dry_run']: @@ -630,9 +630,9 @@ class FreqtradeBot(LoggingMixin): mm_ex_1=0.0, upnl_ex_1=0.0, 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, - mm_rate=mm_rate, + mm_ratio=mm_ratio, ) else: isolated_liq = self.exchange.get_liquidation_price(pair) diff --git a/freqtrade/leverage/liquidation_price.py b/freqtrade/leverage/liquidation_price.py index 30b8885ba..a5a9a6e56 100644 --- a/freqtrade/leverage/liquidation_price.py +++ b/freqtrade/leverage/liquidation_price.py @@ -35,12 +35,12 @@ def liquidation_price( ''' wallet_balance 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. Under the cross margin mode, the same ticker/symbol, - 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 - on the margin allocated to the positions. + 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 on the margin allocated to the positions. position Absolute value of position size (in base currency) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 91f458cb6..ad4a513b4 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -368,7 +368,7 @@ class LocalTrade(): wallet_balance: Optional[float] = None, current_price: 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. @@ -380,6 +380,16 @@ class LocalTrade(): "wallet balance must be passed to LocalTrade.set_isolated_liq when param" "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( exchange_name=self.exchange, open_rate=self.open_rate, @@ -390,9 +400,9 @@ class LocalTrade(): mm_ex_1=0.0, upnl_ex_1=0.0, 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, - mm_rate=mm_rate, + mm_ratio=mm_ratio, ) if isolated_liq is None: diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index ac7647e73..1239f55e0 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -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): 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]], + 'BNB/BUSD': [[0.0, 0.025, 0.0], + [100000.0, 0.05, 2500.0], + [500000.0, 0.1, 27500.0], + [1000000.0, 0.15, 77499.99999999999], + [2000000.0, 0.25, 277500.0], + [5000000.0, 0.5, 1527500.0]], + 'BNB/USDT': [[0.0, 0.0065, 0.0], + [10000.0, 0.01, 35.00000000000001], + [50000.0, 0.02, 535.0], + [250000.0, 0.05, 8035.000000000001], + [1000000.0, 0.1, 58035.0], + [2000000.0, 0.125, 108034.99999999999], + [5000000.0, 0.15, 233034.99999999994], + [10000000.0, 0.25, 1233035.0]], + 'BTC/USDT': [[0.0, 0.004, 0.0], + [50000.0, 0.005, 50.0], + [250000.0, 0.01, 1300.0], + [1000000.0, 0.025, 16300.000000000002], + [5000000.0, 0.05, 141300.0], + [20000000.0, 0.1, 1141300.0], + [50000000.0, 0.125, 2391300.0], + [100000000.0, 0.15, 4891300.0], + [200000000.0, 0.25, 24891300.0], + [300000000.0, 0.5, 99891300.0] + ] + } 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() assert exchange._leverage_brackets == { - 'ADA/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]], - '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]], - "ZEC/USDT": [[0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5]], + 'ADA/BUSD': [[0.0, 0.025, 0.0], + [100000.0, 0.05, 2500.0], + [500000.0, 0.1, 27500.0], + [1000000.0, 0.15, 77499.99999999999], + [2000000.0, 0.25, 277500.0], + [5000000.0, 0.5, 1827500.0]], + 'BTC/USDT': [[0.0, 0.004, 0.0], + [50000.0, 0.005, 50.0], + [250000.0, 0.01, 1300.0], + [1000000.0, 0.025, 16300.000000000002], + [5000000.0, 0.05, 141300.0], + [20000000.0, 0.1, 1141300.0], + [50000000.0, 0.125, 2391300.0], + [100000000.0, 0.15, 4891300.0], + [200000000.0, 0.25, 24891300.0], + [300000000.0, 0.5, 99891300.0]], + "ZEC/USDT": [[0.0, 0.01, 0.0], + [5000.0, 0.025, 75.0], + [25000.0, 0.05, 700.0], + [100000.0, 0.1, 5700.0], + [250000.0, 0.125, 11949.999999999998], + [1000000.0, 0.5, 386950.0]] } api_mock = MagicMock() @@ -389,3 +391,49 @@ def test__ccxt_config(default_conf, mocker, trading_mode, collateral, config): default_conf['collateral'] = collateral exchange = get_patched_exchange(mocker, default_conf, id="binance") 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 diff --git a/tests/leverage/test_liquidation_price.py b/tests/leverage/test_liquidation_price.py index 300c316d9..4707a5680 100644 --- a/tests/leverage/test_liquidation_price.py +++ b/tests/leverage/test_liquidation_price.py @@ -90,7 +90,7 @@ def test_liquidation_price_exception_thrown( @pytest.mark.parametrize( 'exchange_name, is_short, leverage, trading_mode, collateral, wallet_balance, ' '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, 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( 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( exchange_name=exchange_name, @@ -117,5 +117,5 @@ def test_liquidation_price( upnl_ex_1=upnl_ex_1, maintenance_amt=maintenance_amt, position=position, - mm_rate=mm_rate + mm_ratio=mm_ratio ), 2), expected) From bff53c52af21f332a1ec12ff7d7ce445342d041e Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Mon, 10 Jan 2022 12:28:22 -0600 Subject: [PATCH 21/53] rewrite fill_leverage_brackets --- freqtrade/exchange/binance.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 436c6f75e..d17cf7a3f 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -135,17 +135,19 @@ class Binance(Exchange): else: leverage_brackets = self._api.load_leverage_brackets() - for pair, brackets in leverage_brackets.items(): - self._leverage_brackets[pair] = [ - [ - min_amount, - float(margin_req) - ] for [ - min_amount, - margin_req - ] in brackets - ] - + for pair, brkts in leverage_brackets.items(): + [amt, old_ratio] = [None, None] + brackets = [] + for [notional_floor, mm_ratio] in brkts: + amt = ((float(notional_floor) * (mm_ratio - old_ratio)) + + amt) if old_ratio else 0 + old_ratio = mm_ratio + brackets.append([ + float(notional_floor), + mm_ratio, + amt, + ]) + self._leverage_brackets[pair] = brackets except ccxt.DDoSProtection as e: raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: From 888951288777b8e7a65de42961e74c53ba95a811 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Tue, 11 Jan 2022 21:17:29 -0600 Subject: [PATCH 22/53] freqtradebot.leverage_prep gets taker_fee_rate --- freqtrade/freqtradebot.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 80656f209..6e11f3eb1 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -616,10 +616,12 @@ class FreqtradeBot(LoggingMixin): # open_rate=open_rate, # is_short=is_short # ) - mm_ratio, maintenance_amt = self.exchange.get_maintenance_ratio_and_amt(pair, amount) 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 = liquidation_price( exchange_name=self.exchange.name, open_rate=open_rate, @@ -633,6 +635,13 @@ class FreqtradeBot(LoggingMixin): wallet_balance=amount/leverage, # TODO: Update for cross maintenance_amt=maintenance_amt, mm_ratio=mm_ratio, + taker_fee_rate=taker_fee_rate + + # Okex + # liability: Optional[float]=None, + # interest: Optional[float]=None, + # position_assets: Optional[float]=None, # * Might be same as position + ) else: isolated_liq = self.exchange.get_liquidation_price(pair) From 1eee5373b91655385817214c38bc8d2f3c8afcb9 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Tue, 11 Jan 2022 21:22:55 -0600 Subject: [PATCH 23/53] gateio.get_maintenance_ratio_and_amt --- freqtrade/exchange/gateio.py | 13 +++++++++++- tests/exchange/test_gateio.py | 40 +++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/gateio.py b/freqtrade/exchange/gateio.py index 203aa735b..c6dc0c9b9 100644 --- a/freqtrade/exchange/gateio.py +++ b/freqtrade/exchange/gateio.py @@ -1,6 +1,6 @@ """ Gate.io exchange subclass """ import logging -from typing import Dict, List, Tuple +from typing import Dict, List, Optional, Tuple from freqtrade.enums import Collateral, TradingMode from freqtrade.exceptions import OperationalException @@ -40,3 +40,14 @@ class Gateio(Exchange): if any(v == 'market' for k, v in order_types.items()): raise OperationalException( f'Exchange {self.name} does not support market orders.') + + def get_maintenance_ratio_and_amt( + self, + pair: Optional[str], + nominal_value: Optional[float] + ): + info = self.markets[pair]['info'] + if 'maintenance_rate' in info: + return [float(info['maintenance_rate']), None] + else: + return [None, None] diff --git a/tests/exchange/test_gateio.py b/tests/exchange/test_gateio.py index 6f7862909..09bd5e1b3 100644 --- a/tests/exchange/test_gateio.py +++ b/tests/exchange/test_gateio.py @@ -3,6 +3,7 @@ import pytest from freqtrade.exceptions import OperationalException from freqtrade.exchange import Gateio from freqtrade.resolvers.exchange_resolver import ExchangeResolver +from tests.conftest import get_patched_exchange def test_validate_order_types_gateio(default_conf, mocker): @@ -26,3 +27,42 @@ def test_validate_order_types_gateio(default_conf, mocker): with pytest.raises(OperationalException, match=r'Exchange .* does not support market orders.'): ExchangeResolver.load_exchange('gateio', default_conf, True) + + +@pytest.mark.parametrize('pair,mm_ratio', [ + ("ETH/USDT:USDT", 0.005), + ("ADA/USDT:USDT", 0.003), + ("DOGE/USDT:USDT", None), +]) +def test_get_maintenance_ratio_and_amt_gateio(default_conf, mocker, pair, mm_ratio): + exchange = get_patched_exchange(mocker, default_conf, id="gateio") + exchange.markets = { + 'ETH/USDT:USDT': { + 'taker': 0.0000075, + 'maker': -0.0000025, + 'info': { + 'maintenance_rate': '0.005', + }, + 'id': 'ETH_USDT', + 'symbol': 'ETH/USDT:USDT', + }, + 'ADA/USDT:USDT': { + 'taker': 0.0000075, + 'maker': -0.0000025, + 'info': { + 'maintenance_rate': '0.003', + }, + 'id': 'ADA_USDT', + 'symbol': 'ADA/USDT:USDT', + }, + 'DOGE/USDT:USDT': { + 'taker': 0.0000075, + 'maker': -0.0000025, + 'info': { + 'nonmaintenance_rate': '0.003', + }, + 'id': 'DOGE_USDT', + 'symbol': 'DOGE/USDT:USDT', + } + } + assert exchange.get_maintenance_ratio_and_amt_gateio(pair) == [mm_ratio, None] From 2d545a2defd36dd8f8e8240f73d39ccb47a22ab2 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Thu, 13 Jan 2022 01:21:36 -0600 Subject: [PATCH 24/53] fixed breaking tests for liquidation price --- freqtrade/exchange/binance.py | 8 ++-- freqtrade/exchange/exchange.py | 4 +- freqtrade/exchange/gateio.py | 4 +- tests/exchange/test_binance.py | 52 ++++++++++++------------- tests/exchange/test_gateio.py | 70 +++++++++++++++++++--------------- 5 files changed, 73 insertions(+), 65 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index d17cf7a3f..f4923e01d 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -139,12 +139,12 @@ class Binance(Exchange): [amt, old_ratio] = [None, None] brackets = [] for [notional_floor, mm_ratio] in brkts: - amt = ((float(notional_floor) * (mm_ratio - old_ratio)) + + amt = ((float(notional_floor) * (float(mm_ratio) - float(old_ratio))) + amt) if old_ratio else 0 old_ratio = mm_ratio brackets.append([ float(notional_floor), - mm_ratio, + float(mm_ratio), amt, ]) self._leverage_brackets[pair] = brackets @@ -231,8 +231,8 @@ class Binance(Exchange): def get_maintenance_ratio_and_amt( self, - pair: Optional[str], - nominal_value: Optional[float] + pair: str, + nominal_value: Optional[float] = 0.0, ): ''' Maintenance amt = Floor of Position Bracket on Level n * diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 38f3a0b99..870107f0e 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -2005,8 +2005,8 @@ class Exchange: def get_maintenance_ratio_and_amt( self, - pair: Optional[str], - nominal_value: Optional[float] + pair: str, + nominal_value: Optional[float] = 0.0, ): ''' :return: The maintenance amount, and maintenance margin rate diff --git a/freqtrade/exchange/gateio.py b/freqtrade/exchange/gateio.py index c6dc0c9b9..0ae38c52a 100644 --- a/freqtrade/exchange/gateio.py +++ b/freqtrade/exchange/gateio.py @@ -43,8 +43,8 @@ class Gateio(Exchange): def get_maintenance_ratio_and_amt( self, - pair: Optional[str], - nominal_value: Optional[float] + pair: str, + nominal_value: Optional[float] = 0.0, ): info = self.markets[pair]['info'] if 'maintenance_rate' in info: diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index 1239f55e0..46d9ded3d 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -242,7 +242,7 @@ def test_fill_leverage_brackets_binance(default_conf, mocker): [500000.0, 0.1, 27500.0], [1000000.0, 0.15, 77499.99999999999], [2000000.0, 0.25, 277500.0], - [5000000.0, 0.5, 1827500.0]], + [5000000.0, 0.5, 1527500.0]], 'BTC/USDT': [[0.0, 0.004, 0.0], [50000.0, 0.005, 50.0], [250000.0, 0.01, 1300.0], @@ -284,37 +284,37 @@ def test_fill_leverage_brackets_binance_dryrun(default_conf, mocker): leverage_brackets = { "1000SHIB/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] + [0.0, 0.01, 0.0], + [5000.0, 0.025, 75.0], + [25000.0, 0.05, 700.0], + [100000.0, 0.1, 5700.0], + [250000.0, 0.125, 11949.999999999998], + [1000000.0, 0.5, 386950.0], ], "1INCH/USDT": [ - [0.0, 0.012], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] + [0.0, 0.012, 0.0], + [5000.0, 0.025, 65.0], + [25000.0, 0.05, 690.0], + [100000.0, 0.1, 5690.0], + [250000.0, 0.125, 11939.999999999998], + [1000000.0, 0.5, 386940.0], ], "AAVE/USDT": [ - [0.0, 0.01], - [50000.0, 0.02], - [250000.0, 0.05], - [1000000.0, 0.1], - [2000000.0, 0.125], - [5000000.0, 0.1665], - [10000000.0, 0.25] + [0.0, 0.01, 0.0], + [50000.0, 0.02, 500.0], + [250000.0, 0.05, 8000.000000000001], + [1000000.0, 0.1, 58000.0], + [2000000.0, 0.125, 107999.99999999999], + [5000000.0, 0.1665, 315500.00000000006], + [10000000.0, 0.25, 1150500.0], ], "ADA/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] + [0.0, 0.025, 0.0], + [100000.0, 0.05, 2500.0], + [500000.0, 0.1, 27500.0], + [1000000.0, 0.15, 77499.99999999999], + [2000000.0, 0.25, 277500.0], + [5000000.0, 0.5, 1527500.0], ] } diff --git a/tests/exchange/test_gateio.py b/tests/exchange/test_gateio.py index 09bd5e1b3..cf3e44d71 100644 --- a/tests/exchange/test_gateio.py +++ b/tests/exchange/test_gateio.py @@ -1,3 +1,5 @@ +from unittest.mock import MagicMock, PropertyMock + import pytest from freqtrade.exceptions import OperationalException @@ -35,34 +37,40 @@ def test_validate_order_types_gateio(default_conf, mocker): ("DOGE/USDT:USDT", None), ]) def test_get_maintenance_ratio_and_amt_gateio(default_conf, mocker, pair, mm_ratio): - exchange = get_patched_exchange(mocker, default_conf, id="gateio") - exchange.markets = { - 'ETH/USDT:USDT': { - 'taker': 0.0000075, - 'maker': -0.0000025, - 'info': { - 'maintenance_rate': '0.005', - }, - 'id': 'ETH_USDT', - 'symbol': 'ETH/USDT:USDT', - }, - 'ADA/USDT:USDT': { - 'taker': 0.0000075, - 'maker': -0.0000025, - 'info': { - 'maintenance_rate': '0.003', - }, - 'id': 'ADA_USDT', - 'symbol': 'ADA/USDT:USDT', - }, - 'DOGE/USDT:USDT': { - 'taker': 0.0000075, - 'maker': -0.0000025, - 'info': { - 'nonmaintenance_rate': '0.003', - }, - 'id': 'DOGE_USDT', - 'symbol': 'DOGE/USDT:USDT', - } - } - assert exchange.get_maintenance_ratio_and_amt_gateio(pair) == [mm_ratio, None] + mocker.patch( + 'freqtrade.exchange.Exchange.markets', + PropertyMock( + return_value={ + 'ETH/USDT:USDT': { + 'taker': 0.0000075, + 'maker': -0.0000025, + 'info': { + 'maintenance_rate': '0.005', + }, + 'id': 'ETH_USDT', + 'symbol': 'ETH/USDT:USDT', + }, + 'ADA/USDT:USDT': { + 'taker': 0.0000075, + 'maker': -0.0000025, + 'info': { + 'maintenance_rate': '0.003', + }, + 'id': 'ADA_USDT', + 'symbol': 'ADA/USDT:USDT', + }, + 'DOGE/USDT:USDT': { + 'taker': 0.0000075, + 'maker': -0.0000025, + 'info': { + 'nonmaintenance_rate': '0.003', + }, + 'id': 'DOGE_USDT', + 'symbol': 'DOGE/USDT:USDT', + } + } + ) + ) + api_mock = MagicMock() + exchange = get_patched_exchange(mocker, default_conf, api_mock, id="gateio") + assert exchange.get_maintenance_ratio_and_amt(pair) == [mm_ratio, None] From 387a9fbf362c4ed81aaa871a56b14bac617a8979 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Thu, 13 Jan 2022 02:12:02 -0600 Subject: [PATCH 25/53] test_execute_entry liquidation_price test test_get_maintenance_ratio_and_amt_gateio --- freqtrade/constants.py | 2 +- freqtrade/freqtradebot.py | 4 +- freqtrade/leverage/liquidation_price.py | 13 +++--- tests/conftest.py | 12 ++++++ tests/exchange/test_gateio.py | 4 +- tests/test_freqtradebot.py | 54 ++++++++++++++++++++----- 6 files changed, 66 insertions(+), 23 deletions(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 0f841e2a7..434734ef0 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -155,7 +155,7 @@ CONF_SCHEMA = { 'ignore_roi_if_buy_signal': {'type': 'boolean'}, 'ignore_buying_expired_candle_after': {'type': 'number'}, 'trading_mode': {'type': 'string', 'enum': TRADING_MODES}, - 'collateral_type': {'type': 'string', 'enum': COLLATERAL_TYPES}, + 'collateral': {'type': 'string', 'enum': COLLATERAL_TYPES}, 'backtest_breakdown': { 'type': 'array', 'items': {'type': 'string', 'enum': BACKTEST_BREAKDOWNS} diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 6e11f3eb1..82360f429 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -107,8 +107,8 @@ class FreqtradeBot(LoggingMixin): self.trading_mode = TradingMode(self.config.get('trading_mode', 'spot')) self.collateral_type: Optional[Collateral] = None - if 'collateral_type' in self.config: - self.collateral_type = Collateral(self.config['collateral_type']) + if 'collateral' in self.config: + self.collateral_type = Collateral(self.config['collateral']) self._schedule = Scheduler() diff --git a/freqtrade/leverage/liquidation_price.py b/freqtrade/leverage/liquidation_price.py index a5a9a6e56..e52d762eb 100644 --- a/freqtrade/leverage/liquidation_price.py +++ b/freqtrade/leverage/liquidation_price.py @@ -106,8 +106,8 @@ def liquidation_price( trading_mode=trading_mode, collateral=collateral, # type: ignore wallet_balance=wallet_balance, - # mm_ex_1=mm_ex_1, - # upnl_ex_1=upnl_ex_1, + mm_ex_1=mm_ex_1, # type: ignore + upnl_ex_1=upnl_ex_1, # type: ignore maintenance_amt=maintenance_amt, # type: ignore position=position, mm_ratio=mm_ratio, @@ -212,7 +212,6 @@ def binance( :param open_rate: Entry Price of position (one-way mode) :param mm_ratio: Maintenance margin rate of position (one-way mode) """ - # TODO-lev: Additional arguments, fill in formulas wb = wallet_balance tmm_1 = 0.0 if collateral == Collateral.ISOLATED else mm_ex_1 upnl_1 = 0.0 if collateral == Collateral.ISOLATED else upnl_ex_1 @@ -223,7 +222,6 @@ def binance( mmr_b = mm_ratio if trading_mode == TradingMode.MARGIN and collateral == Collateral.CROSS: - # TODO-lev: perform a calculation based on this formula # https://www.binance.com/en/support/faq/f6b010588e55413aa58b7d63ee0125ed exception("binance", trading_mode, collateral) elif trading_mode == TradingMode.FUTURES and collateral == Collateral.ISOLATED: @@ -235,11 +233,11 @@ def binance( position * mmr_b - side_1 * position) elif trading_mode == TradingMode.FUTURES and collateral == Collateral.CROSS: - # TODO-lev: perform a calculation based on this formula # https://www.binance.com/en/support/faq/b3c689c1f50a44cabb3a84e663b81d93 # Liquidation Price of USDⓈ-M Futures Contracts Cross # Isolated margin mode, then TMM=0,UPNL=0 + # * Untested return (wb - tmm_1 + upnl_1 + cum_b - side_1 * position * ep1) / ( position * mmr_b - side_1 * position) @@ -253,18 +251,17 @@ def kraken( leverage: float, trading_mode: TradingMode, collateral: Collateral + # ... ): """ Calculates the liquidation price on Kraken :param trading_mode: spot, margin, futures :param collateral: cross, isolated """ - # TODO-lev: Additional arguments, fill in formulas if collateral == Collateral.CROSS: if trading_mode == TradingMode.MARGIN: exception("kraken", trading_mode, collateral) - # TODO-lev: perform a calculation based on this formula # https://support.kraken.com/hc/en-us/articles/203325763-Margin-Call-Level-and-Margin-Liquidation-Level elif trading_mode == TradingMode.FUTURES: exception("kraken", trading_mode, collateral) @@ -279,6 +276,7 @@ def ftx( leverage: float, trading_mode: TradingMode, collateral: Collateral + # ... ): """ Calculates the liquidation price on FTX @@ -286,7 +284,6 @@ def ftx( :param collateral: cross, isolated """ if collateral == Collateral.CROSS: - # TODO-lev: Additional arguments, fill in formulas exception("ftx", trading_mode, collateral) # If nothing was returned diff --git a/tests/conftest.py b/tests/conftest.py index 207f6ae24..2bacb498e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -823,6 +823,8 @@ def get_markets(): 'margin': True, 'type': 'spot', 'contractSize': None, + 'taker': 0.0006, + 'maker': 0.0002, 'precision': { 'amount': 8, 'price': 8 @@ -860,6 +862,8 @@ def get_markets(): 'margin': True, 'type': 'spot', 'contractSize': None, + 'taker': 0.0006, + 'maker': 0.0002, 'precision': { 'amount': 8, 'price': 8 @@ -892,6 +896,8 @@ def get_markets(): 'active': True, 'spot': True, 'type': 'spot', + 'taker': 0.0006, + 'maker': 0.0002, 'precision': { 'price': 8, 'amount': 8, @@ -923,6 +929,8 @@ def get_markets(): 'active': True, 'spot': True, 'type': 'spot', + 'taker': 0.0006, + 'maker': 0.0002, 'precision': { 'price': 8, 'amount': 8, @@ -955,6 +963,8 @@ def get_markets(): 'spot': True, 'type': 'spot', 'contractSize': None, + 'taker': 0.0006, + 'maker': 0.0002, 'precision': { 'price': 8, 'amount': 8, @@ -1023,6 +1033,8 @@ def get_markets(): 'spot': False, 'type': 'swap', 'contractSize': 0.01, + 'taker': 0.0006, + 'maker': 0.0002, 'precision': { 'amount': 8, 'price': 8 diff --git a/tests/exchange/test_gateio.py b/tests/exchange/test_gateio.py index cf3e44d71..a648b229a 100644 --- a/tests/exchange/test_gateio.py +++ b/tests/exchange/test_gateio.py @@ -37,6 +37,8 @@ def test_validate_order_types_gateio(default_conf, mocker): ("DOGE/USDT:USDT", None), ]) def test_get_maintenance_ratio_and_amt_gateio(default_conf, mocker, pair, mm_ratio): + api_mock = MagicMock() + exchange = get_patched_exchange(mocker, default_conf, api_mock, id="gateio") mocker.patch( 'freqtrade.exchange.Exchange.markets', PropertyMock( @@ -71,6 +73,4 @@ def test_get_maintenance_ratio_and_amt_gateio(default_conf, mocker, pair, mm_rat } ) ) - api_mock = MagicMock() - exchange = get_patched_exchange(mocker, default_conf, api_mock, id="gateio") assert exchange.get_maintenance_ratio_and_amt(pair) == [mm_ratio, None] diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 373ffb215..3651ba7b7 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -707,21 +707,45 @@ def test_process_informative_pairs_added(default_conf_usdt, ticker_usdt, mocker) CandleType.SPOT) in refresh_mock.call_args[0][0] -@pytest.mark.parametrize("trading_mode", [ - 'spot', - # TODO-lev: Enable other modes - # 'margin', 'futures' -] -) -@pytest.mark.parametrize("is_short", [False, True]) +@pytest.mark.parametrize("is_short,trading_mode,exchange_name,margin_mode,liq_price", [ + (False, 'spot', 'binance', '', None), + (True, 'spot', 'binance', '', None), + (False, 'spot', 'gateio', '', None), + (True, 'spot', 'gateio', '', None), + (True, 'futures', 'binance', 'isolated', 13.217821782178218), + (False, 'futures', 'binance', 'isolated', 6.717171717171718), + (True, 'futures', 'gateio', 'isolated', 13.198706526760379), + (False, 'futures', 'gateio', 'isolated', 6.735367414292449), + # TODO-lev: Okex + # (False, 'spot', 'okex', 'isolated', ...), + # (True, 'futures', 'okex', 'isolated', ...), +]) def test_execute_entry(mocker, default_conf_usdt, fee, limit_order, - limit_order_open, is_short, trading_mode) -> None: + limit_order_open, is_short, trading_mode, + exchange_name, margin_mode, liq_price) -> None: + ''' + exchange_name = binance, is_short = true + (wb + cum_b - side_1 * position * ep1) / (position * mmr_b - side_1 * position) + ((2 + 0.01) - ((-1) * 0.6 * 10)) / ((0.6 * 0.01) - ((-1) * 0.6)) = 13.217821782178218 + exchange_name = binance, is_short = false + (wb + cum_b - side_1 * position * ep1) / (position * mmr_b - side_1 * position) + (2 + 0.01 - 1 * 0.6 * 10) / (0.6 * 0.01 - 1 * 0.6) = 6.717171717171718 + + exchange_name = gateio, is_short = true + (open_rate + (wallet_balance / position)) / (1 + (mm_ratio + taker_fee_rate)) + (10 + (6 / 0.6)) / (1 + (0.01 + 0.0002)) + 13.198706526760379 + + exchange_name = gateio, is_short = false + (open_rate - (wallet_balance / position)) / (1 - (mm_ratio + taker_fee_rate)) + (10 - (2 / 0.6)) / (1 - (0.01 + 0.0002)) = 6.735367414292449 + ''' open_order = limit_order_open[enter_side(is_short)] order = limit_order[enter_side(is_short)] default_conf_usdt['trading_mode'] = trading_mode - leverage = 1.0 if trading_mode == 'spot' else 3.0 - default_conf_usdt['collateral'] = 'cross' + leverage = 1.0 if trading_mode == 'spot' else 5.0 + default_conf_usdt['collateral'] = margin_mode patch_RPCManager(mocker) patch_exchange(mocker) freqtrade = FreqtradeBot(default_conf_usdt) @@ -886,14 +910,24 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order, assert trade.open_rate_requested == 10 # In case of custom entry price not float type + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + name=exchange_name, + get_maintenance_ratio_and_amt=MagicMock(return_value=[0.01, 0.01]) + ) order['status'] = 'open' order['id'] = '5568' freqtrade.strategy.custom_entry_price = lambda **kwargs: "string price" assert freqtrade.execute_entry(pair, stake_amount, is_short=is_short) trade = Trade.query.all()[8] + # Trade(id=9, pair=ETH/USDT, amount=0.20000000, is_short=False, + # leverage=1.0, open_rate=10.00000000, open_since=...) + # Trade(id=9, pair=ETH/USDT, amount=0.60000000, is_short=True, + # leverage=3.0, open_rate=10.00000000, open_since=...) trade.is_short = is_short assert trade assert trade.open_rate_requested == 10 + assert trade.isolated_liq == liq_price @pytest.mark.parametrize("is_short", [False, True]) From 7abffee75550d49c7ea8e85eec514c262afae1f7 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Thu, 13 Jan 2022 04:33:34 -0600 Subject: [PATCH 26/53] liquidation_price formula organize and comment clean up --- freqtrade/leverage/liquidation_price.py | 398 +++++++++++------------- 1 file changed, 183 insertions(+), 215 deletions(-) diff --git a/freqtrade/leverage/liquidation_price.py b/freqtrade/leverage/liquidation_price.py index e52d762eb..e4f3874f2 100644 --- a/freqtrade/leverage/liquidation_price.py +++ b/freqtrade/leverage/liquidation_price.py @@ -12,69 +12,59 @@ def liquidation_price( trading_mode: TradingMode, mm_ratio: float, collateral: Optional[Collateral] = Collateral.ISOLATED, - - # Binance - maintenance_amt: Optional[float] = None, - - # Binance and Gateio - wallet_balance: Optional[float] = None, - position: Optional[float] = None, # Absolute value of position size - - # Gateio & Okex - taker_fee_rate: Optional[float] = None, - - # Okex - liability: Optional[float] = None, - interest: Optional[float] = None, - position_assets: Optional[float] = None, # * Might be same as position - - # * Cross only - mm_ex_1: Optional[float] = 0.0, # Cross only - upnl_ex_1: Optional[float] = 0.0, # Cross only + 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]: - ''' - wallet_balance - In Cross margin mode, WB is crossWalletBalance - 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. - Under the cross margin mode, the same ticker/symbol, - 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 on the margin allocated to the positions. - position - Absolute value of position size (in base currency) - - # Binance - maintenance_amt (cumb) - Maintenance Amount of position - - # Gateio & okex & binance - mm_ratio - [assets in the position - (liability +interest) * mark price] / - (maintenance margin + liquidation fee) (okex) + """ + :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) + 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 collateral: Either ISOLATED or CROSS - # Gateio & okex - taker_fee_rate + # * Binance + :param maintenance_amt: (CUM) Maintenance Amount of position - # Okex - liability + # * Binance and Gateio + :param wallet_balance: (WB) + Cross-Margin Mode: crossWalletBalance + Isolated-Margin Mode: isolatedWalletBalance + :param position: Absolute value of position size (in base currency) + + # * Gateio & Okex + :param taker_fee_rate: + + # * Okex + :param liability: Initial liabilities + deducted interest • Long positions: Liability is calculated in quote currency. • Short positions: Liability is calculated in trading currency. - interest + :param interest: Interest that has not been deducted yet. - position_assets + :param position_assets: Total position assets – on-hold by pending order - # * Cross only - mm_ex_1 - Maintenance Margin of all other contracts, excluding Contract 1 - If it is an isolated margin mode, then TMM=0,UPNL=0 - upnl_ex_1 - Unrealized PNL of all other contracts, excluding Contract 1 - If it is an isolated margin mode, then UPNL=0 - ''' + # * 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 @@ -85,20 +75,14 @@ def liquidation_price( ) if exchange_name.lower() == "binance": - if ( - wallet_balance is None or + if (wallet_balance is None or maintenance_amt is None or position is None): # mm_ex_1 is None or # * Cross only # upnl_ex_1 is None or # * Cross only - maintenance_amt is None or - position is None or - mm_ratio is None - ): raise OperationalException( - f"Parameters wallet_balance, mm_ex_1, upnl_ex_1, " - f"maintenance_amt, position, mm_ratio " - f"is required by liquidation_price when exchange is {exchange_name.lower()}") - - # Suppress incompatible type "Optional[float]"; expected "float" as the check exists above. + f"Parameters wallet_balance, maintenance_amt, position" + f"are required by liquidation_price when exchange is {exchange_name.lower()}" + ) + # Suppress incompatible type "Optional[...]"; expected "..." as the check exists above. return binance( open_rate=open_rate, is_short=is_short, @@ -111,21 +95,12 @@ def liquidation_price( maintenance_amt=maintenance_amt, # type: ignore position=position, mm_ratio=mm_ratio, - ) # type: ignore - elif exchange_name.lower() == "kraken": - return kraken(open_rate, is_short, leverage, trading_mode, collateral) - elif exchange_name.lower() == "ftx": - return ftx(open_rate, is_short, leverage, trading_mode, collateral) + ) elif exchange_name.lower() == "gateio": - if ( - not wallet_balance or - not position or - not mm_ratio or - not taker_fee_rate - ): + if (not wallet_balance or not position or not taker_fee_rate): raise OperationalException( - f"{exchange_name} {collateral} {trading_mode} requires parameters " - f"wallet_balance, contract_size, num_contracts, mm_ratio and taker_fee" + f"Parameters wallet_balance, position, taker_fee_rate" + f"are required by liquidation_price when exchange is {exchange_name.lower()}" ) else: return gateio( @@ -139,16 +114,10 @@ def liquidation_price( taker_fee_rate=taker_fee_rate ) elif exchange_name.lower() == "okex": - if ( - not mm_ratio or - not liability or - not interest or - not taker_fee_rate or - not position_assets - ): + if (not liability or not interest or not taker_fee_rate or not position_assets): raise OperationalException( - f"{exchange_name} {collateral} {trading_mode} requires parameters " - f"mm_ratio, liability, interest, taker_fee_rate, position_assets" + f"Parameters liability, interest, taker_fee_rate, position_assets" + f"are required by liquidation_price when exchange is {exchange_name.lower()}" ) else: return okex( @@ -161,9 +130,11 @@ def liquidation_price( taker_fee_rate=taker_fee_rate, position_assets=position_assets, ) - raise OperationalException( - f"liquidation_price is not implemented for {exchange_name}" - ) + elif exchange_name.lower() == "ftx": + return ftx(open_rate, is_short, leverage, trading_mode, collateral) + elif exchange_name.lower() == "kraken": + return kraken(open_rate, is_short, leverage, trading_mode, collateral) + raise OperationalException(f"liquidation_price is not implemented for {exchange_name}") def exception( @@ -172,10 +143,10 @@ def exception( collateral: Collateral, ): """ - Raises an exception if exchange used doesn't support desired leverage mode - :param exchange: Name of the exchange - :param trading_mode: spot, margin, futures - :param collateral: cross, isolated + Raises an exception if exchange used doesn't support desired leverage mode + :param exchange: Name of the exchange + :param trading_mode: spot, margin, futures + :param collateral: cross, isolated """ raise OperationalException( @@ -187,132 +158,79 @@ def binance( is_short: bool, leverage: float, trading_mode: TradingMode, + mm_ratio: float, collateral: Collateral, + maintenance_amt: float, wallet_balance: float, + position: float, mm_ex_1: float, upnl_ex_1: float, - maintenance_amt: float, - position: float, - mm_ratio: float, ): """ - Calculates the liquidation price on Binance + MARGIN: https://www.binance.com/en/support/faq/f6b010588e55413aa58b7d63ee0125ed + PERPETUAL: https://www.binance.com/en/support/faq/b3c689c1f50a44cabb3a84e663b81d93 + + :param open_rate: (EP1) Entry Price of position (one-way mode) :param is_short: true or false :param leverage: leverage in float - :param trading_mode: spot, margin, futures - :param collateral: cross, isolated - :param wallet_balance: Wallet Balance is crossWalletBalance in Cross-Margin Mode. - Wallet Balance is isolatedWalletBalance in Isolated Margin Mode - :param mm_ex_1: Maintenance Margin of all other contracts, - excluding Contract 1. If it is an isolated margin mode, then TMM=0 - :param upnl_ex_1: Unrealized PNL of all other contracts, excluding Contract 1. - If it is an isolated margin mode, then UPNL=0 - :param maintenance_amt: Maintenance Amount of position (one-way mode) + :param trading_mode: SPOT, MARGIN, FUTURES + :param mm_ratio: (MMR) Maintenance margin rate of position (one-way mode) + :param collateral: CROSS, ISOLATED + :param maintenance_amt: (CUM) Maintenance Amount of position (one-way mode) :param position: Absolute value of position size (one-way mode) - :param open_rate: Entry Price of position (one-way mode) - :param mm_ratio: Maintenance margin rate of position (one-way mode) + :param wallet_balance: (WB) + Cross-Margin Mode: crossWalletBalance + Isolated-Margin Mode: isolatedWalletBalance + TMM=0, UPNL=0, substitute the position quantity, MMR, cum into the formula to calculate. + Under the cross margin mode, the same ticker/symbol, + both long and short position share the same liquidation price except in isolated mode. + Under the isolated mode, each isolated position will have different liquidation prices + depending on the margin allocated to the positions. + :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 """ - wb = wallet_balance - tmm_1 = 0.0 if collateral == Collateral.ISOLATED else mm_ex_1 - upnl_1 = 0.0 if collateral == Collateral.ISOLATED else upnl_ex_1 - cum_b = maintenance_amt side_1 = -1 if is_short else 1 position = abs(position) - ep1 = open_rate - mmr_b = mm_ratio + cross_vars = upnl_ex_1 - mm_ex_1 if collateral == Collateral.CROSS else 0.0 if trading_mode == TradingMode.MARGIN and collateral == Collateral.CROSS: - # https://www.binance.com/en/support/faq/f6b010588e55413aa58b7d63ee0125ed + # ! Not Implemented exception("binance", trading_mode, collateral) - elif trading_mode == TradingMode.FUTURES and collateral == Collateral.ISOLATED: - # https://www.binance.com/en/support/faq/b3c689c1f50a44cabb3a84e663b81d93 - # Liquidation Price of USDⓈ-M Futures Contracts Isolated + if trading_mode == TradingMode.FUTURES: + return (wallet_balance + cross_vars + maintenance_amt - (side_1 * position * open_rate)) / ( + (position * mm_ratio) - (side_1 * position)) - # Isolated margin mode, then TMM=0,UPNL=0 - return (wb + cum_b - side_1 * position * ep1) / ( - position * mmr_b - side_1 * position) - - elif trading_mode == TradingMode.FUTURES and collateral == Collateral.CROSS: - # https://www.binance.com/en/support/faq/b3c689c1f50a44cabb3a84e663b81d93 - # Liquidation Price of USDⓈ-M Futures Contracts Cross - - # Isolated margin mode, then TMM=0,UPNL=0 - # * Untested - return (wb - tmm_1 + upnl_1 + cum_b - side_1 * position * ep1) / ( - position * mmr_b - side_1 * position) - - # If nothing was returned exception("binance", trading_mode, collateral) -def kraken( - open_rate: float, - is_short: bool, - leverage: float, - trading_mode: TradingMode, - collateral: Collateral - # ... -): - """ - Calculates the liquidation price on Kraken - :param trading_mode: spot, margin, futures - :param collateral: cross, isolated - """ - - if collateral == Collateral.CROSS: - if trading_mode == TradingMode.MARGIN: - exception("kraken", trading_mode, collateral) - # https://support.kraken.com/hc/en-us/articles/203325763-Margin-Call-Level-and-Margin-Liquidation-Level - elif trading_mode == TradingMode.FUTURES: - exception("kraken", trading_mode, collateral) - - # If nothing was returned - exception("kraken", trading_mode, collateral) - - -def ftx( - open_rate: float, - is_short: bool, - leverage: float, - trading_mode: TradingMode, - collateral: Collateral - # ... -): - """ - Calculates the liquidation price on FTX - :param trading_mode: spot, margin, futures - :param collateral: cross, isolated - """ - if collateral == Collateral.CROSS: - exception("ftx", trading_mode, collateral) - - # If nothing was returned - exception("ftx", trading_mode, collateral) - - def gateio( open_rate: float, is_short: bool, trading_mode: TradingMode, - collateral: Collateral, - wallet_balance: float, - position: float, mm_ratio: float, + collateral: Collateral, + position: float, + wallet_balance: float, taker_fee_rate: float, is_inverse: bool = False ): """ - Calculates the liquidation price on Gate.io + PERPETUAL: https://www.gate.io/help/futures/perpetual/22160/calculation-of-liquidation-price + :param open_rate: Entry Price of position :param is_short: True for short trades - :param trading_mode: spot, margin, futures - :param collateral: cross, isolated - :param wallet_balance: Also called margin + :param trading_mode: SPOT, MARGIN, FUTURES + :param mm_ratio: Viewed in contract details + :param collateral: CROSS, ISOLATED :param position: size of position in base currency contract_size / num_contracts contract_size: How much one contract is worth num_contracts: Also called position - :param mm_ratio: Viewed in contract details + :param wallet_balance: Also called margin :param taker_fee_rate: :param is_inverse: True if settle currency matches base currency @@ -320,16 +238,13 @@ def gateio( '±' in the formula refers to the direction of the contract, go long refers to '-' go short refers to '+' - Position refers to the number of contracts. - Maintenance Margin Ratio and Contract Multiplier can be viewed in the Contract Details. - https://www.gate.io/help/futures/perpetual/22160/calculation-of-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") + # ! Not implemented + raise OperationalException("Freqtrade does not support inverse contracts at the moment") value = wallet_balance / position mm_ratio_taker = (mm_ratio + taker_fee_rate) @@ -344,24 +259,40 @@ def gateio( def okex( is_short: bool, trading_mode: TradingMode, + mm_ratio: float, collateral: Collateral, + taker_fee_rate: float, liability: float, interest: float, - mm_ratio: float, - taker_fee_rate: float, position_assets: float ): ''' - https://www.okex.com/support/hc/en-us/articles/ + PERPETUAL: https://www.okex.com/support/hc/en-us/articles/ 360053909592-VI-Introduction-to-the-isolated-mode-of-Single-Multi-currency-Portfolio-margin - Initial liabilities + deducted interest - Long positions: Liability is calculated in quote currency. - Short positions: Liability is calculated in trading currency. - interest: Interest that has not been deducted yet. - Margin ratio - Long: [position_assets - (liability + interest) / mark_price] / (maintenance_margin + fees) - Short: [position_assets - (liability + interest) * mark_price] / (maintenance_margin + fees) + :param is_short: True if the position is short, false otherwise + :param trading_mode: SPOT, MARGIN, FUTURES + :param mm_ratio: + long: [position_assets - (liability + interest) / mark_price] / (maintenance_margin + fees) + short: [position_assets - (liability + interest) * mark_price] / (maintenance_margin + fees) + :param collateral: CROSS, ISOLATED + :param taker_fee_rate: + :param liability: Initial liabilities + deducted interest + long: Liability is calculated in quote currency + short: Liability is calculated in trading currency + :param interest: Interest that has not been deducted yet + :param position_assets: Total position assets - on-hold by pending order + + Total: The number of positive assets on the position (including margin). + long: with trading currency as position asset. + short: with quote currency as position asset. + + Est. liquidation price + long: (liability + interest)* (1 + maintenance margin ratio) * + (1 + taker fee rate) / position assets + short: (liability + interest)* (1 + maintenance margin ratio) * + (1 + taker fee rate) + ''' if trading_mode == TradingMode.FUTURES and collateral == Collateral.ISOLATED: if is_short: @@ -371,18 +302,55 @@ def okex( else: exception("okex", trading_mode, collateral) -# if __name__ == '__main__': -# print(liquidation_price( -# "binance", -# 32481.980, -# False, -# 1, -# TradingMode.FUTURES, -# Collateral.ISOLATED, -# 1535443.01, -# 356512.508, -# 0.0, -# 16300.000, -# 109.488, -# 0.025 -# )) + +def ftx( + open_rate: float, + is_short: bool, + leverage: float, + trading_mode: TradingMode, + collateral: Collateral + # ... +): + """ + # ! Not Implemented + Calculates the liquidation price on FTX + :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 trading_mode: SPOT, MARGIN, FUTURES, etc. + :param collateral: Either ISOLATED or CROSS + """ + if collateral == Collateral.CROSS: + exception("ftx", trading_mode, collateral) + + # If nothing was returned + exception("ftx", trading_mode, collateral) + + +def kraken( + open_rate: float, + is_short: bool, + leverage: float, + trading_mode: TradingMode, + collateral: Collateral + # ... +): + """ + # ! Not Implemented + MARGIN: https://support.kraken.com/hc/en-us/articles/203325763-Margin-Call-Level-and-Margin-Liquidation-Level + + :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 trading_mode: SPOT, MARGIN, FUTURES, etc. + :param collateral: Either ISOLATED or CROSS + """ + + if collateral == Collateral.CROSS: + if trading_mode == TradingMode.MARGIN: + exception("kraken", trading_mode, collateral) + elif trading_mode == TradingMode.FUTURES: + exception("kraken", trading_mode, collateral) + + # If nothing was returned + exception("kraken", trading_mode, collateral) From 0c8205ab3bd98e4e5866b72099803debcab52a66 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Fri, 14 Jan 2022 05:49:46 -0600 Subject: [PATCH 27/53] replace single quote docstrings with double quote docstrings --- freqtrade/exchange/binance.py | 11 ++-- freqtrade/exchange/exchange.py | 14 ++--- freqtrade/leverage/liquidation_price.py | 4 +- tests/exchange/test_exchange.py | 68 ++++++++++++------------- tests/test_freqtradebot.py | 8 +-- 5 files changed, 54 insertions(+), 51 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index f4923e01d..a0b246096 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -139,8 +139,11 @@ class Binance(Exchange): [amt, old_ratio] = [None, None] brackets = [] for [notional_floor, mm_ratio] in brkts: - amt = ((float(notional_floor) * (float(mm_ratio) - float(old_ratio))) + - amt) if old_ratio else 0 + amt = ( + ( + (float(notional_floor) * (float(mm_ratio)) - float(old_ratio)) + ) + amt + ) if old_ratio else 0 old_ratio = mm_ratio brackets.append([ float(notional_floor), @@ -234,14 +237,14 @@ class Binance(Exchange): pair: str, nominal_value: Optional[float] = 0.0, ): - ''' + """ 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] diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 870107f0e..c7600a591 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1985,10 +1985,10 @@ class Exchange: @retrier def get_liquidation_price(self, pair: str): - ''' - 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") - ''' + """ + 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._config['dry_run'] or not self.exchange_has("fetchPositions"): # Some exchanges only support one collateral type return @@ -2008,9 +2008,9 @@ class Exchange: pair: str, nominal_value: Optional[float] = 0.0, ): - ''' - :return: The maintenance amount, and maintenance margin rate - ''' + """ + :return: The maintenance amount, and maintenance margin rate + """ # TODO-lev: return the real amounts return 0, 0.4 diff --git a/freqtrade/leverage/liquidation_price.py b/freqtrade/leverage/liquidation_price.py index e4f3874f2..c71e876db 100644 --- a/freqtrade/leverage/liquidation_price.py +++ b/freqtrade/leverage/liquidation_price.py @@ -266,7 +266,7 @@ def okex( interest: float, position_assets: float ): - ''' + """ PERPETUAL: https://www.okex.com/support/hc/en-us/articles/ 360053909592-VI-Introduction-to-the-isolated-mode-of-Single-Multi-currency-Portfolio-margin @@ -293,7 +293,7 @@ def okex( short: (liability + interest)* (1 + maintenance margin ratio) * (1 + taker fee rate) - ''' + """ if trading_mode == TradingMode.FUTURES and collateral == Collateral.ISOLATED: if is_short: return (liability + interest) * (1 + mm_ratio) * (1 + taker_fee_rate) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index b529af47f..de92e1614 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -3638,41 +3638,41 @@ def test__fetch_and_calculate_funding_fees( amount, expected_fees ): - ''' - nominal_value = mark_price * size - funding_fee = nominal_value * funding_rate - size: 30 - time: 0, mark: 2.77, nominal_value: 83.1, fundRate: -0.000008, fundFee: -0.0006648 - time: 1, mark: 2.73, nominal_value: 81.9, fundRate: -0.000004, fundFee: -0.0003276 - time: 2, mark: 2.74, nominal_value: 82.2, fundRate: 0.000012, fundFee: 0.0009864 - time: 3, mark: 2.76, nominal_value: 82.8, fundRate: -0.000003, fundFee: -0.0002484 - time: 4, mark: 2.76, nominal_value: 82.8, fundRate: -0.000007, fundFee: -0.0005796 - time: 5, mark: 2.77, nominal_value: 83.1, fundRate: 0.000003, fundFee: 0.0002493 - time: 6, mark: 2.78, nominal_value: 83.4, fundRate: 0.000019, fundFee: 0.0015846 - time: 7, mark: 2.78, nominal_value: 83.4, fundRate: 0.000003, fundFee: 0.0002502 - time: 8, mark: 2.77, nominal_value: 83.1, fundRate: -0.000003, fundFee: -0.0002493 - time: 9, mark: 2.77, nominal_value: 83.1, fundRate: 0, fundFee: 0.0 - time: 10, mark: 2.84, nominal_value: 85.2, fundRate: 0.000013, fundFee: 0.0011076 - time: 11, mark: 2.81, nominal_value: 84.3, fundRate: 0.000077, fundFee: 0.0064911 - time: 12, mark: 2.81, nominal_value: 84.3, fundRate: 0.000072, fundFee: 0.0060696 - time: 13, mark: 2.82, nominal_value: 84.6, fundRate: 0.000097, fundFee: 0.0082062 + """ + nominal_value = mark_price * size + funding_fee = nominal_value * funding_rate + size: 30 + time: 0, mark: 2.77, nominal_value: 83.1, fundRate: -0.000008, fundFee: -0.0006648 + time: 1, mark: 2.73, nominal_value: 81.9, fundRate: -0.000004, fundFee: -0.0003276 + time: 2, mark: 2.74, nominal_value: 82.2, fundRate: 0.000012, fundFee: 0.0009864 + time: 3, mark: 2.76, nominal_value: 82.8, fundRate: -0.000003, fundFee: -0.0002484 + time: 4, mark: 2.76, nominal_value: 82.8, fundRate: -0.000007, fundFee: -0.0005796 + time: 5, mark: 2.77, nominal_value: 83.1, fundRate: 0.000003, fundFee: 0.0002493 + time: 6, mark: 2.78, nominal_value: 83.4, fundRate: 0.000019, fundFee: 0.0015846 + time: 7, mark: 2.78, nominal_value: 83.4, fundRate: 0.000003, fundFee: 0.0002502 + time: 8, mark: 2.77, nominal_value: 83.1, fundRate: -0.000003, fundFee: -0.0002493 + time: 9, mark: 2.77, nominal_value: 83.1, fundRate: 0, fundFee: 0.0 + time: 10, mark: 2.84, nominal_value: 85.2, fundRate: 0.000013, fundFee: 0.0011076 + time: 11, mark: 2.81, nominal_value: 84.3, fundRate: 0.000077, fundFee: 0.0064911 + time: 12, mark: 2.81, nominal_value: 84.3, fundRate: 0.000072, fundFee: 0.0060696 + time: 13, mark: 2.82, nominal_value: 84.6, fundRate: 0.000097, fundFee: 0.0082062 - size: 50 - time: 0, mark: 2.77, nominal_value: 138.5, fundRate: -0.000008, fundFee: -0.001108 - time: 1, mark: 2.73, nominal_value: 136.5, fundRate: -0.000004, fundFee: -0.000546 - time: 2, mark: 2.74, nominal_value: 137.0, fundRate: 0.000012, fundFee: 0.001644 - time: 3, mark: 2.76, nominal_value: 138.0, fundRate: -0.000003, fundFee: -0.000414 - time: 4, mark: 2.76, nominal_value: 138.0, fundRate: -0.000007, fundFee: -0.000966 - time: 5, mark: 2.77, nominal_value: 138.5, fundRate: 0.000003, fundFee: 0.0004155 - time: 6, mark: 2.78, nominal_value: 139.0, fundRate: 0.000019, fundFee: 0.002641 - time: 7, mark: 2.78, nominal_value: 139.0, fundRate: 0.000003, fundFee: 0.000417 - time: 8, mark: 2.77, nominal_value: 138.5, fundRate: -0.000003, fundFee: -0.0004155 - time: 9, mark: 2.77, nominal_value: 138.5, fundRate: 0, fundFee: 0.0 - time: 10, mark: 2.84, nominal_value: 142.0, fundRate: 0.000013, fundFee: 0.001846 - time: 11, mark: 2.81, nominal_value: 140.5, fundRate: 0.000077, fundFee: 0.0108185 - time: 12, mark: 2.81, nominal_value: 140.5, fundRate: 0.000072, fundFee: 0.010116 - time: 13, mark: 2.82, nominal_value: 141.0, fundRate: 0.000097, fundFee: 0.013677 - ''' + size: 50 + time: 0, mark: 2.77, nominal_value: 138.5, fundRate: -0.000008, fundFee: -0.001108 + time: 1, mark: 2.73, nominal_value: 136.5, fundRate: -0.000004, fundFee: -0.000546 + time: 2, mark: 2.74, nominal_value: 137.0, fundRate: 0.000012, fundFee: 0.001644 + time: 3, mark: 2.76, nominal_value: 138.0, fundRate: -0.000003, fundFee: -0.000414 + time: 4, mark: 2.76, nominal_value: 138.0, fundRate: -0.000007, fundFee: -0.000966 + time: 5, mark: 2.77, nominal_value: 138.5, fundRate: 0.000003, fundFee: 0.0004155 + time: 6, mark: 2.78, nominal_value: 139.0, fundRate: 0.000019, fundFee: 0.002641 + time: 7, mark: 2.78, nominal_value: 139.0, fundRate: 0.000003, fundFee: 0.000417 + time: 8, mark: 2.77, nominal_value: 138.5, fundRate: -0.000003, fundFee: -0.0004155 + time: 9, mark: 2.77, nominal_value: 138.5, fundRate: 0, fundFee: 0.0 + time: 10, mark: 2.84, nominal_value: 142.0, fundRate: 0.000013, fundFee: 0.001846 + time: 11, mark: 2.81, nominal_value: 140.5, fundRate: 0.000077, fundFee: 0.0108185 + time: 12, mark: 2.81, nominal_value: 140.5, fundRate: 0.000072, fundFee: 0.010116 + time: 13, mark: 2.82, nominal_value: 141.0, fundRate: 0.000097, fundFee: 0.013677 + """ d1 = datetime.strptime(f"{d1} +0000", '%Y-%m-%d %H:%M:%S %z') d2 = datetime.strptime(f"{d2} +0000", '%Y-%m-%d %H:%M:%S %z') funding_rate_history = { diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 3651ba7b7..624a07f5e 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -723,7 +723,7 @@ def test_process_informative_pairs_added(default_conf_usdt, ticker_usdt, mocker) def test_execute_entry(mocker, default_conf_usdt, fee, limit_order, limit_order_open, is_short, trading_mode, exchange_name, margin_mode, liq_price) -> None: - ''' + """ exchange_name = binance, is_short = true (wb + cum_b - side_1 * position * ep1) / (position * mmr_b - side_1 * position) ((2 + 0.01) - ((-1) * 0.6 * 10)) / ((0.6 * 0.01) - ((-1) * 0.6)) = 13.217821782178218 @@ -740,7 +740,7 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order, exchange_name = gateio, is_short = false (open_rate - (wallet_balance / position)) / (1 - (mm_ratio + taker_fee_rate)) (10 - (2 / 0.6)) / (1 - (0.01 + 0.0002)) = 6.735367414292449 - ''' + """ open_order = limit_order_open[enter_side(is_short)] order = limit_order[enter_side(is_short)] default_conf_usdt['trading_mode'] = trading_mode @@ -4828,7 +4828,7 @@ def test_update_funding_fees( limit_order_open, schedule_off ): - ''' + """ nominal_value = mark_price * size funding_fee = nominal_value * funding_rate size = 123 @@ -4844,7 +4844,7 @@ def test_update_funding_fees( "XRP/BTC" time: 0, mark: 1.2, fundRate: 0.00049426, nominal_value: 147.6, fundFee: 0.072952776 time: 8, mark: 1.2, fundRate: 0.00032715, nominal_value: 147.6, fundFee: 0.04828734 - ''' + """ # SETUP time_machine.move_to("2021-09-01 00:00:00 +00:00") From b4a0611afc6a6ac17d56264df176fe85dbeb94bc Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Fri, 14 Jan 2022 05:52:16 -0600 Subject: [PATCH 28/53] exchange.get_liquidation_price removed irrelevant comment --- freqtrade/exchange/exchange.py | 1 - 1 file changed, 1 deletion(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index c7600a591..f312b8d63 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1990,7 +1990,6 @@ class Exchange: :param pair: base/quote currency pair (e.g. "ADA/USDT") """ if self._config['dry_run'] or not self.exchange_has("fetchPositions"): - # Some exchanges only support one collateral type return try: From bb2b2211d09c4b2b29627ef30a4848a4bf3614cb Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Fri, 14 Jan 2022 06:11:17 -0600 Subject: [PATCH 29/53] exchange.fill_leverage_brackets/get_maintenance_ratio_and_amt docstring and type specification --- freqtrade/exchange/binance.py | 33 +++++++++++++++++++++---- freqtrade/exchange/exchange.py | 8 +++--- freqtrade/exchange/gateio.py | 10 ++++---- freqtrade/freqtradebot.py | 6 +++-- freqtrade/leverage/liquidation_price.py | 3 ++- tests/exchange/test_binance.py | 4 +-- tests/exchange/test_gateio.py | 22 ++++++++--------- tests/test_freqtradebot.py | 2 +- 8 files changed, 57 insertions(+), 31 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index a0b246096..0b434d9d3 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -119,10 +119,25 @@ class Binance(Exchange): raise OperationalException(e) from e @retrier - def fill_leverage_brackets(self): + def fill_leverage_brackets(self) -> None: """ Assigns property _leverage_brackets to a dictionary of information about the leverage allowed on each pair + After exectution, self._leverage_brackets = { + "pair_name": [ + [notional_floor, maintenenace_margin_ratio, maintenance_amt], + ... + ], + ... + } + e.g. { + "ETH/USDT:USDT": [ + [0.0, 0.01, 0.0], + [10000, 0.02, 0.01], + ... + ], + ... + } """ if self.trading_mode == TradingMode.FUTURES: try: @@ -136,14 +151,14 @@ class Binance(Exchange): leverage_brackets = self._api.load_leverage_brackets() for pair, brkts in leverage_brackets.items(): - [amt, old_ratio] = [None, None] + [amt, old_ratio] = [0.0, 0.0] brackets = [] for [notional_floor, mm_ratio] in brkts: amt = ( ( (float(notional_floor) * (float(mm_ratio)) - float(old_ratio)) ) + amt - ) if old_ratio else 0 + ) if old_ratio else 0.0 old_ratio = mm_ratio brackets.append([ float(notional_floor), @@ -167,6 +182,9 @@ class Binance(Exchange): """ if pair not in self._leverage_brackets: return 1.0 + if (pair is None or nominal_value is None): + raise OperationalException( + "binance.get_max_leverage requires parameters pair and nominal_value") pair_brackets = self._leverage_brackets[pair] for [notional_floor, mm_ratio, _] in reversed(pair_brackets): if nominal_value >= notional_floor: @@ -236,15 +254,20 @@ class Binance(Exchange): self, pair: str, nominal_value: Optional[float] = 0.0, - ): + ) -> Tuple[float, Optional[float]]: """ + Formula: https://www.binance.com/en/support/faq/b3c689c1f50a44cabb3a84e663b81d93 + 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 + :return: The maintenance margin ratio and maintenance amount """ + if nominal_value is None: + raise OperationalException( + "nominal value is required for binance.get_maintenance_ratio_and_amt") if pair not in self._leverage_brackets: raise InvalidOrderException(f"Cannot calculate liquidation price for {pair}") pair_brackets = self._leverage_brackets[pair] diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index f312b8d63..9d9105c2c 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -90,7 +90,7 @@ class Exchange: self._api: ccxt.Exchange = None self._api_async: ccxt_async.Exchange = None self._markets: Dict = {} - self._leverage_brackets: Dict = {} + self._leverage_brackets: Dict[str, List[List[float]]] = {} self.loop = asyncio.new_event_loop() asyncio.set_event_loop(self.loop) @@ -2006,12 +2006,12 @@ class Exchange: self, pair: str, nominal_value: Optional[float] = 0.0, - ): + ) -> Tuple[float, Optional[float]]: """ - :return: The maintenance amount, and maintenance margin rate + :return: The maintenance margin ratio and maintenance amount """ # TODO-lev: return the real amounts - return 0, 0.4 + return (0, 0.4) def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = None) -> bool: diff --git a/freqtrade/exchange/gateio.py b/freqtrade/exchange/gateio.py index 0ae38c52a..c62b6222d 100644 --- a/freqtrade/exchange/gateio.py +++ b/freqtrade/exchange/gateio.py @@ -45,9 +45,9 @@ class Gateio(Exchange): self, pair: str, nominal_value: Optional[float] = 0.0, - ): + ) -> Tuple[float, Optional[float]]: + """ + :return: The maintenance margin ratio and maintenance amount + """ info = self.markets[pair]['info'] - if 'maintenance_rate' in info: - return [float(info['maintenance_rate']), None] - else: - return [None, None] + return (float(info['maintenance_rate']), None) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 82360f429..9937b700b 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -620,7 +620,9 @@ class FreqtradeBot(LoggingMixin): if self.collateral_type == Collateral.ISOLATED: if self.config['dry_run']: mm_ratio, maintenance_amt = self.exchange.get_maintenance_ratio_and_amt( - pair, amount) + pair, + amount + ) taker_fee_rate = self.exchange.markets[pair]['taker'] isolated_liq = liquidation_price( exchange_name=self.exchange.name, @@ -637,7 +639,7 @@ class FreqtradeBot(LoggingMixin): mm_ratio=mm_ratio, taker_fee_rate=taker_fee_rate - # Okex + # TODO-lev: Okex parameters # liability: Optional[float]=None, # interest: Optional[float]=None, # position_assets: Optional[float]=None, # * Might be same as position diff --git a/freqtrade/leverage/liquidation_price.py b/freqtrade/leverage/liquidation_price.py index c71e876db..c80367a37 100644 --- a/freqtrade/leverage/liquidation_price.py +++ b/freqtrade/leverage/liquidation_price.py @@ -337,7 +337,8 @@ def kraken( ): """ # ! Not Implemented - MARGIN: https://support.kraken.com/hc/en-us/articles/203325763-Margin-Call-Level-and-Margin-Liquidation-Level + MARGIN: + https://support.kraken.com/hc/en-us/articles/203325763-Margin-Call-Level-and-Margin-Liquidation-Level :param open_rate: Entry price of position :param is_short: True if the trade is a short, false otherwise diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index 46d9ded3d..15ee56013 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -407,7 +407,7 @@ def test_get_maintenance_ratio_and_amt_binance( pair, nominal_value, mm_ratio, - amt + amt, ): exchange = get_patched_exchange(mocker, default_conf, id="binance") exchange._leverage_brackets = { @@ -436,4 +436,4 @@ def test_get_maintenance_ratio_and_amt_binance( [200000000.0, 0.25], [300000000.0, 0.5]], } - assert exchange.get_max_leverage(pair, nominal_value) == max_lev + assert exchange.get_maintenance_ratio_and_amt(pair, nominal_value) == (mm_ratio, amt) diff --git a/tests/exchange/test_gateio.py b/tests/exchange/test_gateio.py index a648b229a..96ae37598 100644 --- a/tests/exchange/test_gateio.py +++ b/tests/exchange/test_gateio.py @@ -34,7 +34,7 @@ def test_validate_order_types_gateio(default_conf, mocker): @pytest.mark.parametrize('pair,mm_ratio', [ ("ETH/USDT:USDT", 0.005), ("ADA/USDT:USDT", 0.003), - ("DOGE/USDT:USDT", None), + # ("DOGE/USDT:USDT", None), ]) def test_get_maintenance_ratio_and_amt_gateio(default_conf, mocker, pair, mm_ratio): api_mock = MagicMock() @@ -61,16 +61,16 @@ def test_get_maintenance_ratio_and_amt_gateio(default_conf, mocker, pair, mm_rat 'id': 'ADA_USDT', 'symbol': 'ADA/USDT:USDT', }, - 'DOGE/USDT:USDT': { - 'taker': 0.0000075, - 'maker': -0.0000025, - 'info': { - 'nonmaintenance_rate': '0.003', - }, - 'id': 'DOGE_USDT', - 'symbol': 'DOGE/USDT:USDT', - } + # 'DOGE/USDT:USDT': { + # 'taker': 0.0000075, + # 'maker': -0.0000025, + # 'info': { + # 'nonmaintenance_rate': '0.003', + # }, + # 'id': 'DOGE_USDT', + # 'symbol': 'DOGE/USDT:USDT', + # } } ) ) - assert exchange.get_maintenance_ratio_and_amt(pair) == [mm_ratio, None] + assert exchange.get_maintenance_ratio_and_amt(pair) == (mm_ratio, None) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 624a07f5e..6f47a62aa 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -913,7 +913,7 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order, mocker.patch.multiple( 'freqtrade.exchange.Exchange', name=exchange_name, - get_maintenance_ratio_and_amt=MagicMock(return_value=[0.01, 0.01]) + get_maintenance_ratio_and_amt=MagicMock(return_value=(0.01, 0.01)) ) order['status'] = 'open' order['id'] = '5568' From c2f92015124865fce9592db05b7b0dee50a2a010 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Fri, 14 Jan 2022 07:16:00 -0600 Subject: [PATCH 30/53] Added get_liquidation_price check --- freqtrade/exchange/exchange.py | 4 +++- tests/exchange/test_exchange.py | 33 +++++++++++++++++++++++++++++++-- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 9d9105c2c..eea00aa79 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1993,7 +1993,9 @@ class Exchange: return try: - return self._api.fetch_positions(pair).liquidationPrice + positions = self._api.fetch_positions([pair]) + position = positions[0] + return position['liquidationPrice'] except ccxt.DDoSProtection as e: raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index de92e1614..95400dd3d 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -3583,10 +3583,39 @@ def test_calculate_funding_fees( def test_get_liquidation_price(mocker, default_conf): api_mock = MagicMock() - api_mock.fetch_positions = MagicMock() - type(api_mock).has = PropertyMock(return_value={'fetchPositions': True}) + api_mock.fetch_positions = MagicMock(return_value=[{ + 'info': {}, + 'symbol': 'NEAR/USDT:USDT', + 'timestamp': 1642164737148, + 'datetime': '2022-01-14T12:52:17.148Z', + 'initialMargin': 1.51072, + 'initialMarginPercentage': 0.1, + 'maintenanceMargin': 0.38916147, + 'maintenanceMarginPercentage': 0.025, + 'entryPrice': 18.884, + 'notional': 15.1072, + 'leverage': 9.97, + 'unrealizedPnl': 0.0048, + 'contracts': 8, + 'contractSize': 0.1, + 'marginRatio': None, + 'liquidationPrice': 17.47, + 'markPrice': 18.89, + 'collateral': 1.52549075, + 'marginType': 'isolated', + 'side': 'buy', + 'percentage': 0.003177292946409658 + }]) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + exchange_has=MagicMock(return_value=True), + ) default_conf['dry_run'] = False + exchange = get_patched_exchange(mocker, default_conf) + liq_price = exchange.get_liquidation_price('NEAR/USDT:USDT') + assert liq_price == 17.47 + ccxt_exceptionhandlers( mocker, default_conf, From 1f1ac8ce9dae0e1a14435069fb110c9d55aad5c1 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Fri, 14 Jan 2022 06:58:32 -0600 Subject: [PATCH 31/53] test_get_liquidation_price/test_get_maintenance_ratio_and_amt_binance/fill_leverage_brackets/test_validate_trading_mode_and_collateral TODO comments --- freqtrade/exchange/binance.py | 2 +- tests/exchange/test_binance.py | 55 ++++++++++++++------------- tests/exchange/test_exchange.py | 67 +++++++++++++++++++-------------- tests/test_freqtradebot.py | 37 +++++++++--------- 4 files changed, 87 insertions(+), 74 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 0b434d9d3..e1623025a 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -156,7 +156,7 @@ class Binance(Exchange): for [notional_floor, mm_ratio] in brkts: amt = ( ( - (float(notional_floor) * (float(mm_ratio)) - float(old_ratio)) + (float(notional_floor) * (float(mm_ratio) - float(old_ratio))) ) + amt ) if old_ratio else 0.0 old_ratio = mm_ratio diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index 15ee56013..886c11980 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -397,9 +397,9 @@ def test__ccxt_config(default_conf, mocker, trading_mode, collateral, config): ("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), + ("BNB/BUSD", 999999.9, 0.1, 27500.0), + ("BNB/USDT", 5000000.0, 0.15, 233034.99999999994), + ("BTC/USDT", 300000000.1, 0.5, 99891300.0), ]) def test_get_maintenance_ratio_and_amt_binance( default_conf, @@ -411,29 +411,30 @@ def test_get_maintenance_ratio_and_amt_binance( ): 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]], + 'BNB/BUSD': [[0.0, 0.025, 0.0], + [100000.0, 0.05, 2500.0], + [500000.0, 0.1, 27500.0], + [1000000.0, 0.15, 77499.99999999999], + [2000000.0, 0.25, 277500.0], + [5000000.0, 0.5, 1527500.0]], + 'BNB/USDT': [[0.0, 0.0065, 0.0], + [10000.0, 0.01, 35.00000000000001], + [50000.0, 0.02, 535.0], + [250000.0, 0.05, 8035.000000000001], + [1000000.0, 0.1, 58035.0], + [2000000.0, 0.125, 108034.99999999999], + [5000000.0, 0.15, 233034.99999999994], + [10000000.0, 0.25, 1233035.0]], + 'BTC/USDT': [[0.0, 0.004, 0.0], + [50000.0, 0.005, 50.0], + [250000.0, 0.01, 1300.0], + [1000000.0, 0.025, 16300.000000000002], + [5000000.0, 0.05, 141300.0], + [20000000.0, 0.1, 1141300.0], + [50000000.0, 0.125, 2391300.0], + [100000000.0, 0.15, 4891300.0], + [200000000.0, 0.25, 24891300.0], + [300000000.0, 0.5, 99891300.0] + ] } assert exchange.get_maintenance_ratio_and_amt(pair, nominal_value) == (mm_ratio, amt) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 95400dd3d..6e0c02e7c 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -3438,8 +3438,18 @@ def test_set_margin_mode(mocker, default_conf, collateral): ("bittrex", TradingMode.FUTURES, Collateral.CROSS, True), ("bittrex", TradingMode.FUTURES, Collateral.ISOLATED, True), ("gateio", TradingMode.MARGIN, Collateral.ISOLATED, True), + ("okex", TradingMode.SPOT, None, False), + ("okex", TradingMode.MARGIN, Collateral.CROSS, True), + ("okex", TradingMode.MARGIN, Collateral.ISOLATED, True), + ("okex", TradingMode.FUTURES, Collateral.CROSS, True), - # TODO-lev: Remove once implemented + ("binance", TradingMode.FUTURES, Collateral.ISOLATED, False), + ("gateio", TradingMode.FUTURES, Collateral.ISOLATED, False), + + # ("okex", TradingMode.FUTURES, Collateral.ISOLATED, False), # TODO-lev: uncomment once impleme + ("okex", TradingMode.FUTURES, Collateral.ISOLATED, True), # TODO-lev: remove once implemented + + # * Remove once implemented ("binance", TradingMode.MARGIN, Collateral.CROSS, True), ("binance", TradingMode.FUTURES, Collateral.CROSS, True), ("kraken", TradingMode.MARGIN, Collateral.CROSS, True), @@ -3449,17 +3459,15 @@ def test_set_margin_mode(mocker, default_conf, collateral): ("gateio", TradingMode.MARGIN, Collateral.CROSS, True), ("gateio", TradingMode.FUTURES, Collateral.CROSS, True), - # TODO-lev: Uncomment once implemented + # * Uncomment once implemented # ("binance", TradingMode.MARGIN, Collateral.CROSS, False), # ("binance", TradingMode.FUTURES, Collateral.CROSS, False), - ("binance", TradingMode.FUTURES, Collateral.ISOLATED, False), # ("kraken", TradingMode.MARGIN, Collateral.CROSS, False), # ("kraken", TradingMode.FUTURES, Collateral.CROSS, False), # ("ftx", TradingMode.MARGIN, Collateral.CROSS, False), # ("ftx", TradingMode.FUTURES, Collateral.CROSS, False), # ("gateio", TradingMode.MARGIN, Collateral.CROSS, False), # ("gateio", TradingMode.FUTURES, Collateral.CROSS, False), - ("gateio", TradingMode.FUTURES, Collateral.ISOLATED, False), ]) def test_validate_trading_mode_and_collateral( default_conf, @@ -3583,36 +3591,39 @@ def test_calculate_funding_fees( def test_get_liquidation_price(mocker, default_conf): api_mock = MagicMock() - api_mock.fetch_positions = MagicMock(return_value=[{ - 'info': {}, - 'symbol': 'NEAR/USDT:USDT', - 'timestamp': 1642164737148, - 'datetime': '2022-01-14T12:52:17.148Z', - 'initialMargin': 1.51072, - 'initialMarginPercentage': 0.1, - 'maintenanceMargin': 0.38916147, - 'maintenanceMarginPercentage': 0.025, - 'entryPrice': 18.884, - 'notional': 15.1072, - 'leverage': 9.97, - 'unrealizedPnl': 0.0048, - 'contracts': 8, - 'contractSize': 0.1, - 'marginRatio': None, - 'liquidationPrice': 17.47, - 'markPrice': 18.89, - 'collateral': 1.52549075, - 'marginType': 'isolated', - 'side': 'buy', - 'percentage': 0.003177292946409658 - }]) + positions = [ + { + 'info': {}, + 'symbol': 'NEAR/USDT:USDT', + 'timestamp': 1642164737148, + 'datetime': '2022-01-14T12:52:17.148Z', + 'initialMargin': 1.51072, + 'initialMarginPercentage': 0.1, + 'maintenanceMargin': 0.38916147, + 'maintenanceMarginPercentage': 0.025, + 'entryPrice': 18.884, + 'notional': 15.1072, + 'leverage': 9.97, + 'unrealizedPnl': 0.0048, + 'contracts': 8, + 'contractSize': 0.1, + 'marginRatio': None, + 'liquidationPrice': 17.47, + 'markPrice': 18.89, + 'collateral': 1.52549075, + 'marginType': 'isolated', + 'side': 'buy', + 'percentage': 0.003177292946409658 + } + ] + api_mock.fetch_positions = MagicMock(return_value=positions) mocker.patch.multiple( 'freqtrade.exchange.Exchange', exchange_has=MagicMock(return_value=True), ) default_conf['dry_run'] = False - exchange = get_patched_exchange(mocker, default_conf) + exchange = get_patched_exchange(mocker, default_conf, api_mock) liq_price = exchange.get_liquidation_price('NEAR/USDT:USDT') assert liq_price == 17.47 diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 6f47a62aa..7d859f955 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -708,10 +708,10 @@ def test_process_informative_pairs_added(default_conf_usdt, ticker_usdt, mocker) @pytest.mark.parametrize("is_short,trading_mode,exchange_name,margin_mode,liq_price", [ - (False, 'spot', 'binance', '', None), - (True, 'spot', 'binance', '', None), - (False, 'spot', 'gateio', '', None), - (True, 'spot', 'gateio', '', None), + (False, 'spot', 'binance', None, None), + (True, 'spot', 'binance', None, None), + (False, 'spot', 'gateio', None, None), + (True, 'spot', 'gateio', None, None), (True, 'futures', 'binance', 'isolated', 13.217821782178218), (False, 'futures', 'binance', 'isolated', 6.717171717171718), (True, 'futures', 'gateio', 'isolated', 13.198706526760379), @@ -745,7 +745,8 @@ 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['collateral'] = margin_mode + if margin_mode: + default_conf_usdt['collateral'] = margin_mode patch_RPCManager(mocker) patch_exchange(mocker) freqtrade = FreqtradeBot(default_conf_usdt) @@ -4832,16 +4833,16 @@ def test_update_funding_fees( nominal_value = mark_price * size funding_fee = nominal_value * funding_rate size = 123 - "LTC/BTC" + "LTC/USDT" time: 0, mark: 3.3, fundRate: 0.00032583, nominal_value: 405.9, fundFee: 0.132254397 time: 8, mark: 3.2, fundRate: 0.00024472, nominal_value: 393.6, fundFee: 0.096321792 - "ETH/BTC" + "ETH/USDT" time: 0, mark: 2.4, fundRate: 0.0001, nominal_value: 295.2, fundFee: 0.02952 time: 8, mark: 2.5, fundRate: 0.0001, nominal_value: 307.5, fundFee: 0.03075 - "ETC/BTC" + "ETC/USDT" time: 0, mark: 4.3, fundRate: 0.00031077, nominal_value: 528.9, fundFee: 0.164366253 time: 8, mark: 4.1, fundRate: 0.00022655, nominal_value: 504.3, fundFee: 0.114249165 - "XRP/BTC" + "XRP/USDT" time: 0, mark: 1.2, fundRate: 0.00049426, nominal_value: 147.6, fundFee: 0.072952776 time: 8, mark: 1.2, fundRate: 0.00032715, nominal_value: 147.6, fundFee: 0.04828734 """ @@ -4865,19 +4866,19 @@ def test_update_funding_fees( # 16:00 entry is actually never used # But should be kept in the test to ensure we're filtering correctly. funding_rates = { - "LTC/BTC": + "LTC/USDT": DataFrame([ [date_midnight, 0.00032583, 0, 0, 0, 0], [date_eight, 0.00024472, 0, 0, 0, 0], [date_sixteen, 0.00024472, 0, 0, 0, 0], ], columns=columns), - "ETH/BTC": + "ETH/USDT": DataFrame([ [date_midnight, 0.0001, 0, 0, 0, 0], [date_eight, 0.0001, 0, 0, 0, 0], [date_sixteen, 0.0001, 0, 0, 0, 0], ], columns=columns), - "XRP/BTC": + "XRP/USDT": DataFrame([ [date_midnight, 0.00049426, 0, 0, 0, 0], [date_eight, 0.00032715, 0, 0, 0, 0], @@ -4886,19 +4887,19 @@ def test_update_funding_fees( } mark_prices = { - "LTC/BTC": + "LTC/USDT": DataFrame([ [date_midnight, 3.3, 0, 0, 0, 0], [date_eight, 3.2, 0, 0, 0, 0], [date_sixteen, 3.2, 0, 0, 0, 0], ], columns=columns), - "ETH/BTC": + "ETH/USDT": DataFrame([ [date_midnight, 2.4, 0, 0, 0, 0], [date_eight, 2.5, 0, 0, 0, 0], [date_sixteen, 2.5, 0, 0, 0, 0], ], columns=columns), - "XRP/BTC": + "XRP/USDT": DataFrame([ [date_midnight, 1.2, 0, 0, 0, 0], [date_eight, 1.2, 0, 0, 0, 0], @@ -4935,9 +4936,9 @@ def test_update_funding_fees( freqtrade = get_patched_freqtradebot(mocker, default_conf) # initial funding fees, - freqtrade.execute_entry('ETH/BTC', 123, is_short=is_short) - freqtrade.execute_entry('LTC/BTC', 2.0, is_short=is_short) - freqtrade.execute_entry('XRP/BTC', 123, is_short=is_short) + freqtrade.execute_entry('ETH/USDT', 123, is_short=is_short) + freqtrade.execute_entry('LTC/USDT', 2.0, is_short=is_short) + freqtrade.execute_entry('XRP/USDT', 123, is_short=is_short) multipl = 1 if is_short else -1 trades = Trade.get_open_trades() assert len(trades) == 3 From e4b37c64629ab8d3bcf820d728ddb42c161cb95f Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sat, 15 Jan 2022 10:23:20 -0600 Subject: [PATCH 32/53] freqtradebot.leverage_prep minor fixes --- freqtrade/freqtradebot.py | 4 ++-- tests/test_freqtradebot.py | 32 ++++++++++++++++---------------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 9937b700b..26bcb456c 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -633,8 +633,8 @@ class FreqtradeBot(LoggingMixin): collateral=Collateral.ISOLATED, mm_ex_1=0.0, upnl_ex_1=0.0, - position=amount * open_rate, - wallet_balance=amount/leverage, # TODO: Update for cross + position=amount, + wallet_balance=(amount * open_rate)/leverage, # TODO: Update for cross maintenance_amt=maintenance_amt, mm_ratio=mm_ratio, taker_fee_rate=taker_fee_rate diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 7d859f955..affc86692 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -712,10 +712,10 @@ def test_process_informative_pairs_added(default_conf_usdt, ticker_usdt, mocker) (True, 'spot', 'binance', None, None), (False, 'spot', 'gateio', None, None), (True, 'spot', 'gateio', None, None), - (True, 'futures', 'binance', 'isolated', 13.217821782178218), - (False, 'futures', 'binance', 'isolated', 6.717171717171718), - (True, 'futures', 'gateio', 'isolated', 13.198706526760379), - (False, 'futures', 'gateio', 'isolated', 6.735367414292449), + (True, 'futures', 'binance', 'isolated', 11.89108910891089), + (False, 'futures', 'binance', 'isolated', 8.070707070707071), + (True, 'futures', 'gateio', 'isolated', 11.87413417771621), + (False, 'futures', 'gateio', 'isolated', 8.085708510208207), # TODO-lev: Okex # (False, 'spot', 'okex', 'isolated', ...), # (True, 'futures', 'okex', 'isolated', ...), @@ -725,21 +725,23 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order, exchange_name, margin_mode, liq_price) -> None: """ exchange_name = binance, is_short = true - (wb + cum_b - side_1 * position * ep1) / (position * mmr_b - side_1 * position) - ((2 + 0.01) - ((-1) * 0.6 * 10)) / ((0.6 * 0.01) - ((-1) * 0.6)) = 13.217821782178218 + leverage = 5 + position = 0.2 * 5 + ((wb + cum_b) - (side_1 * position * ep1)) / ((position * mmr_b) - (side_1 * position)) + ((2 + 0.01) - ((-1) * 1 * 10)) / ((1 * 0.01) - ((-1) * 1)) = 11.89108910891089 exchange_name = binance, is_short = false - (wb + cum_b - side_1 * position * ep1) / (position * mmr_b - side_1 * position) - (2 + 0.01 - 1 * 0.6 * 10) / (0.6 * 0.01 - 1 * 0.6) = 6.717171717171718 + ((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 (open_rate + (wallet_balance / position)) / (1 + (mm_ratio + taker_fee_rate)) - (10 + (6 / 0.6)) / (1 + (0.01 + 0.0002)) - 13.198706526760379 + (10 + (2 / 1)) / (1 + (0.01 + 0.0006)) = 11.87413417771621 exchange_name = gateio, is_short = false (open_rate - (wallet_balance / position)) / (1 - (mm_ratio + taker_fee_rate)) - (10 - (2 / 0.6)) / (1 - (0.01 + 0.0002)) = 6.735367414292449 + (10 - (2 / 1)) / (1 - (0.01 + 0.0006)) = 8.085708510208207 """ open_order = limit_order_open[enter_side(is_short)] order = limit_order[enter_side(is_short)] @@ -768,6 +770,7 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order, get_min_pair_stake_amount=MagicMock(return_value=1), get_fee=fee, get_funding_fees=MagicMock(return_value=0), + name=exchange_name ) pair = 'ETH/USDT' @@ -911,11 +914,8 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order, assert trade.open_rate_requested == 10 # In case of custom entry price not float type - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - name=exchange_name, - get_maintenance_ratio_and_amt=MagicMock(return_value=(0.01, 0.01)) - ) + freqtrade.exchange.get_maintenance_ratio_and_amt = MagicMock(return_value=(0.01, 0.01)) + freqtrade.exchange.name = exchange_name order['status'] = 'open' order['id'] = '5568' freqtrade.strategy.custom_entry_price = lambda **kwargs: "string price" From caff7e227f81c5e974ee257c14ce3c97e36d1d11 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Wed, 19 Jan 2022 21:36:54 -0600 Subject: [PATCH 33/53] binance.fill_leverage_brackets remove excess bracket --- freqtrade/exchange/binance.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index e1623025a..76dd2fef4 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -155,9 +155,8 @@ class Binance(Exchange): brackets = [] for [notional_floor, mm_ratio] in brkts: amt = ( - ( - (float(notional_floor) * (float(mm_ratio) - float(old_ratio))) - ) + amt + (float(notional_floor) * (float(mm_ratio) - float(old_ratio))) + + amt ) if old_ratio else 0.0 old_ratio = mm_ratio brackets.append([ From 1f8111d1c671b74b1ba666cf93e244c598a2eaf8 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Wed, 19 Jan 2022 21:41:43 -0600 Subject: [PATCH 34/53] exchange.get_max_leverage pair is required --- freqtrade/exchange/binance.py | 5 +---- freqtrade/exchange/exchange.py | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 76dd2fef4..9bb00443c 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -173,7 +173,7 @@ class Binance(Exchange): except ccxt.BaseError as e: raise OperationalException(e) from e - def get_max_leverage(self, pair: Optional[str], nominal_value: Optional[float]) -> float: + def get_max_leverage(self, pair: str, nominal_value: float) -> float: """ Returns the maximum leverage that a pair can be traded at :param pair: The base/quote currency pair being traded @@ -181,9 +181,6 @@ class Binance(Exchange): """ if pair not in self._leverage_brackets: return 1.0 - if (pair is None or nominal_value is None): - raise OperationalException( - "binance.get_max_leverage requires parameters pair and nominal_value") pair_brackets = self._leverage_brackets[pair] for [notional_floor, mm_ratio, _] in reversed(pair_brackets): if nominal_value >= notional_floor: diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index eea00aa79..631546f49 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1807,7 +1807,7 @@ class Exchange: """ return - def get_max_leverage(self, pair: Optional[str], nominal_value: Optional[float]) -> float: + def get_max_leverage(self, pair: str, nominal_value: float) -> float: """ Returns the maximum leverage that a pair can be traded at :param pair: The base/quote currency pair being traded From 5a97760bd1f2eae75d906701edbf225c9de6d14f Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Wed, 19 Jan 2022 21:50:06 -0600 Subject: [PATCH 35/53] binance.get_max_leverage divide by 0 warning --- freqtrade/exchange/binance.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 9bb00443c..55168aebf 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -184,7 +184,10 @@ class Binance(Exchange): pair_brackets = self._leverage_brackets[pair] for [notional_floor, mm_ratio, _] in reversed(pair_brackets): if nominal_value >= notional_floor: - return 1/mm_ratio + if mm_ratio != 0: + return 1/mm_ratio + else: + logger.warning(f"mm_ratio for {pair} with nominal_value {nominal_value} is 0") return 1.0 @retrier From 0c13e387fe16ddf01cab8f1ac87199508a851cdd Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sat, 22 Jan 2022 20:03:38 -0600 Subject: [PATCH 36/53] moved liquidation_price method to exchange classes --- freqtrade/exchange/exchange.py | 108 +++++++ freqtrade/exchange/gateio.py | 75 +++++ freqtrade/exchange/okex.py | 67 ++++- freqtrade/freqtradebot.py | 4 +- freqtrade/leverage/__init__.py | 1 - freqtrade/leverage/liquidation_price.py | 357 ----------------------- freqtrade/persistence/models.py | 46 +-- tests/exchange/test_exchange.py | 105 +++++++ tests/leverage/test_liquidation_price.py | 121 -------- tests/test_freqtradebot.py | 1 - 10 files changed, 358 insertions(+), 527 deletions(-) delete mode 100644 freqtrade/leverage/liquidation_price.py delete mode 100644 tests/leverage/test_liquidation_price.py diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 631546f49..e277c0428 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -2015,6 +2015,114 @@ class Exchange: # TODO-lev: return the real amounts return (0, 0.4) + def liquidation_price( + self, + open_rate: float, # Entry price of position + is_short: bool, + leverage: float, + trading_mode: TradingMode, + mm_ratio: float, + collateral: Optional[Collateral] = Collateral.ISOLATED, + 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]: + """ + :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) + 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 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 + :param position: Absolute value of position size (in base currency) + + # * Gateio & Okex + :param taker_fee_rate: + + # * Okex + :param liability: + Initial liabilities + deducted interest + • Long positions: Liability is calculated in quote currency. + • Short positions: Liability is calculated in trading currency. + :param interest: + Interest that has not been deducted yet. + :param position_assets: + Total position assets – on-hold by pending order + + # * 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, + is_short, + leverage, + trading_mode, + mm_ratio, + collateral, + maintenance_amt, + position, + wallet_balance, + taker_fee_rate, + liability, + interest, + position_assets, + mm_ex_1, + upnl_ex_1, + ) + + def liquidation_price_helper( + self, + open_rate: float, + is_short: bool, + leverage: float, + trading_mode: TradingMode, + mm_ratio: float, + collateral: Collateral, + maintenance_amt: Optional[float] = None, + position: Optional[float] = None, + wallet_balance: Optional[float] = None, + taker_fee_rate: Optional[float] = None, + liability: Optional[float] = None, + interest: Optional[float] = None, + position_assets: Optional[float] = None, + mm_ex_1: Optional[float] = 0.0, + upnl_ex_1: Optional[float] = 0.0, + ) -> Optional[float]: + raise OperationalException(f"liquidation_price is not implemented for {self.name}") + def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = None) -> bool: return exchange_name in ccxt_exchanges(ccxt_module) diff --git a/freqtrade/exchange/gateio.py b/freqtrade/exchange/gateio.py index c62b6222d..e2dbf35c7 100644 --- a/freqtrade/exchange/gateio.py +++ b/freqtrade/exchange/gateio.py @@ -51,3 +51,78 @@ class Gateio(Exchange): """ info = self.markets[pair]['info'] return (float(info['maintenance_rate']), None) + + 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]: + """ + PERPETUAL: https://www.gate.io/help/futures/perpetual/22160/calculation-of-liquidation-price + + :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 trading_mode: SPOT, MARGIN, FUTURES, etc. + :param position: Absolute value of position size (in base currency) + :param mm_ratio: + :param collateral: Either ISOLATED or CROSS + :param maintenance_amt: # * Not required by Gateio + :param wallet_balance: + Cross-Margin Mode: crossWalletBalance + Isolated-Margin Mode: isolatedWalletBalance + :param position: Absolute value of position size (in base currency) + :param taker_fee_rate: + + # * Not required by Gateio + :param liability: + :param interest: + :param position_assets: + :param mm_ex_1: + :param upnl_ex_1: + """ + 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 (not wallet_balance or not position or not taker_fee_rate): + raise OperationalException( + "Parameters wallet_balance, position, taker_fee_rate" + "are required by Gateio.liquidation_price" + ) + + if trading_mode == TradingMode.FUTURES and collateral == Collateral.ISOLATED: + # if is_inverse: + # # ! Not implemented + # 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) + else: + raise OperationalException( + f"Gateio does not support {collateral.value} Mode {trading_mode.value} trading ") diff --git a/freqtrade/exchange/okex.py b/freqtrade/exchange/okex.py index 7e9348f0c..f847f1180 100644 --- a/freqtrade/exchange/okex.py +++ b/freqtrade/exchange/okex.py @@ -1,7 +1,8 @@ import logging -from typing import Dict, List, Tuple +from typing import Dict, List, Optional, Tuple from freqtrade.enums import Collateral, TradingMode +from freqtrade.exceptions import OperationalException from freqtrade.exchange import Exchange @@ -26,3 +27,67 @@ class Okex(Exchange): # (TradingMode.FUTURES, Collateral.CROSS), # (TradingMode.FUTURES, Collateral.ISOLATED) ] + + 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, # Not required + position: Optional[float] = None, # Not required + wallet_balance: Optional[float] = None, # Not required + taker_fee_rate: Optional[float] = None, # * required + liability: Optional[float] = None, # * required + interest: Optional[float] = None, # * required + position_assets: Optional[float] = None, # * required (Might be same as position) + mm_ex_1: Optional[float] = 0.0, # Not required + upnl_ex_1: Optional[float] = 0.0, # Not required + ) -> Optional[float]: + """ + PERPETUAL: 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 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: + Okex: [assets in the position - (liability +interest) * mark price] / + (maintenance margin + liquidation fee) + :param collateral: Either ISOLATED or CROSS + :param maintenance_amt: # * Not required by Okex + :param wallet_balance: # * Not required by Okex + :param position: # * Not required by Okex + :param taker_fee_rate: + :param liability: + Initial liabilities + deducted interest + • Long positions: Liability is calculated in quote currency. + • Short positions: Liability is calculated in trading currency. + :param interest: Interest that has not been deducted yet. + :param position_assets: Total position assets – on-hold by pending order + :param mm_ex_1: # * Not required by Okex + :param upnl_ex_1: # * Not required by Okex + """ + + if (not liability or not interest or not taker_fee_rate or not position_assets): + raise OperationalException( + "Parameters liability, interest, taker_fee_rate, position_assets" + "are required by Okex.liquidation_price" + ) + + if trading_mode == TradingMode.FUTURES and collateral == Collateral.ISOLATED: + if is_short: + return (liability + interest) * (1 + mm_ratio) * (1 + taker_fee_rate) + else: + return ( + (liability + interest) * (1 + mm_ratio) * (1 + taker_fee_rate) / + position_assets + ) + else: + raise OperationalException( + f"Okex does not support {collateral.value} Mode {trading_mode.value} trading") diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 26bcb456c..2911b8ea4 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -21,7 +21,6 @@ from freqtrade.enums import (Collateral, RPCMessageType, RunMode, SellType, Sign from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError, InvalidOrderException, PricingError) from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds -from freqtrade.leverage import liquidation_price from freqtrade.misc import safe_value_fallback, safe_value_fallback2 from freqtrade.mixins import LoggingMixin from freqtrade.persistence import Order, PairLocks, Trade, cleanup_db, init_db @@ -624,8 +623,7 @@ class FreqtradeBot(LoggingMixin): amount ) taker_fee_rate = self.exchange.markets[pair]['taker'] - isolated_liq = liquidation_price( - exchange_name=self.exchange.name, + isolated_liq = self.exchange.liquidation_price( open_rate=open_rate, is_short=is_short, leverage=leverage, diff --git a/freqtrade/leverage/__init__.py b/freqtrade/leverage/__init__.py index 0bb2dd0be..ae78f4722 100644 --- a/freqtrade/leverage/__init__.py +++ b/freqtrade/leverage/__init__.py @@ -1,3 +1,2 @@ # flake8: noqa: F401 from freqtrade.leverage.interest import interest -from freqtrade.leverage.liquidation_price import liquidation_price diff --git a/freqtrade/leverage/liquidation_price.py b/freqtrade/leverage/liquidation_price.py deleted file mode 100644 index c80367a37..000000000 --- a/freqtrade/leverage/liquidation_price.py +++ /dev/null @@ -1,357 +0,0 @@ -from typing import Optional - -from freqtrade.enums import Collateral, TradingMode -from freqtrade.exceptions import OperationalException - - -def liquidation_price( - exchange_name: str, - open_rate: float, # Entry price of position - is_short: bool, - leverage: float, - trading_mode: TradingMode, - mm_ratio: float, - collateral: Optional[Collateral] = Collateral.ISOLATED, - 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]: - """ - :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) - 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 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 - :param position: Absolute value of position size (in base currency) - - # * Gateio & Okex - :param taker_fee_rate: - - # * Okex - :param liability: - Initial liabilities + deducted interest - • Long positions: Liability is calculated in quote currency. - • Short positions: Liability is calculated in trading currency. - :param interest: - Interest that has not been deducted yet. - :param position_assets: - Total position assets – on-hold by pending order - - # * 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}" - ) - - if exchange_name.lower() == "binance": - if (wallet_balance is None or maintenance_amt is None or position is None): - # mm_ex_1 is None or # * Cross only - # upnl_ex_1 is None or # * Cross only - raise OperationalException( - f"Parameters wallet_balance, maintenance_amt, position" - f"are required by liquidation_price when exchange is {exchange_name.lower()}" - ) - # Suppress incompatible type "Optional[...]"; expected "..." as the check exists above. - return binance( - open_rate=open_rate, - is_short=is_short, - leverage=leverage, - trading_mode=trading_mode, - collateral=collateral, # type: ignore - wallet_balance=wallet_balance, - mm_ex_1=mm_ex_1, # type: ignore - upnl_ex_1=upnl_ex_1, # type: ignore - maintenance_amt=maintenance_amt, # type: ignore - position=position, - mm_ratio=mm_ratio, - ) - elif exchange_name.lower() == "gateio": - if (not wallet_balance or not position or not taker_fee_rate): - raise OperationalException( - f"Parameters wallet_balance, position, taker_fee_rate" - f"are required by liquidation_price when exchange is {exchange_name.lower()}" - ) - else: - return gateio( - open_rate=open_rate, - is_short=is_short, - trading_mode=trading_mode, - collateral=collateral, - wallet_balance=wallet_balance, - position=position, - mm_ratio=mm_ratio, - taker_fee_rate=taker_fee_rate - ) - elif exchange_name.lower() == "okex": - if (not liability or not interest or not taker_fee_rate or not position_assets): - raise OperationalException( - f"Parameters liability, interest, taker_fee_rate, position_assets" - f"are required by liquidation_price when exchange is {exchange_name.lower()}" - ) - else: - return okex( - is_short=is_short, - trading_mode=trading_mode, - collateral=collateral, - mm_ratio=mm_ratio, - liability=liability, - interest=interest, - taker_fee_rate=taker_fee_rate, - position_assets=position_assets, - ) - elif exchange_name.lower() == "ftx": - return ftx(open_rate, is_short, leverage, trading_mode, collateral) - elif exchange_name.lower() == "kraken": - return kraken(open_rate, is_short, leverage, trading_mode, collateral) - raise OperationalException(f"liquidation_price is not implemented for {exchange_name}") - - -def exception( - exchange: str, - trading_mode: TradingMode, - collateral: Collateral, -): - """ - Raises an exception if exchange used doesn't support desired leverage mode - :param exchange: Name of the exchange - :param trading_mode: spot, margin, futures - :param collateral: cross, isolated - """ - - raise OperationalException( - f"{exchange} does not support {collateral.value} Mode {trading_mode.value} trading ") - - -def binance( - open_rate: float, - is_short: bool, - leverage: float, - trading_mode: TradingMode, - mm_ratio: float, - collateral: Collateral, - maintenance_amt: float, - wallet_balance: float, - position: float, - mm_ex_1: float, - upnl_ex_1: float, -): - """ - MARGIN: https://www.binance.com/en/support/faq/f6b010588e55413aa58b7d63ee0125ed - PERPETUAL: https://www.binance.com/en/support/faq/b3c689c1f50a44cabb3a84e663b81d93 - - :param open_rate: (EP1) Entry Price of position (one-way mode) - :param is_short: true or false - :param leverage: leverage in float - :param trading_mode: SPOT, MARGIN, FUTURES - :param mm_ratio: (MMR) Maintenance margin rate of position (one-way mode) - :param collateral: CROSS, ISOLATED - :param maintenance_amt: (CUM) Maintenance Amount of position (one-way mode) - :param position: Absolute value of position size (one-way mode) - :param wallet_balance: (WB) - Cross-Margin Mode: crossWalletBalance - Isolated-Margin Mode: isolatedWalletBalance - TMM=0, UPNL=0, substitute the position quantity, MMR, cum into the formula to calculate. - Under the cross margin mode, the same ticker/symbol, - both long and short position share the same liquidation price except in isolated mode. - Under the isolated mode, each isolated position will have different liquidation prices - depending on the margin allocated to the positions. - :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 - """ - 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 - - if trading_mode == TradingMode.MARGIN and collateral == Collateral.CROSS: - # ! Not Implemented - exception("binance", trading_mode, collateral) - if trading_mode == TradingMode.FUTURES: - return (wallet_balance + cross_vars + maintenance_amt - (side_1 * position * open_rate)) / ( - (position * mm_ratio) - (side_1 * position)) - - exception("binance", trading_mode, collateral) - - -def gateio( - open_rate: float, - is_short: bool, - trading_mode: TradingMode, - mm_ratio: float, - collateral: Collateral, - position: float, - wallet_balance: float, - taker_fee_rate: float, - is_inverse: bool = False -): - """ - PERPETUAL: https://www.gate.io/help/futures/perpetual/22160/calculation-of-liquidation-price - - :param open_rate: Entry Price of position - :param is_short: True for short trades - :param trading_mode: SPOT, MARGIN, FUTURES - :param mm_ratio: Viewed in contract details - :param collateral: CROSS, ISOLATED - :param position: size of position in base currency - contract_size / num_contracts - contract_size: How much one contract is worth - num_contracts: Also called position - :param wallet_balance: Also called margin - :param taker_fee_rate: - :param is_inverse: True if settle currency matches base currency - - ( Opening Price ± Margin/Contract Multiplier/Position ) / [ 1 ± ( MMR + Taker Fee)] - '±' in the formula refers to the direction of the contract, - go long refers to '-' - go short refers to '+' - - """ - - if trading_mode == TradingMode.FUTURES and collateral == Collateral.ISOLATED: - if is_inverse: - # ! Not implemented - 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) - else: - exception("gatio", trading_mode, collateral) - - -def okex( - is_short: bool, - trading_mode: TradingMode, - mm_ratio: float, - collateral: Collateral, - taker_fee_rate: float, - liability: float, - interest: float, - position_assets: float -): - """ - PERPETUAL: https://www.okex.com/support/hc/en-us/articles/ - 360053909592-VI-Introduction-to-the-isolated-mode-of-Single-Multi-currency-Portfolio-margin - - :param is_short: True if the position is short, false otherwise - :param trading_mode: SPOT, MARGIN, FUTURES - :param mm_ratio: - long: [position_assets - (liability + interest) / mark_price] / (maintenance_margin + fees) - short: [position_assets - (liability + interest) * mark_price] / (maintenance_margin + fees) - :param collateral: CROSS, ISOLATED - :param taker_fee_rate: - :param liability: Initial liabilities + deducted interest - long: Liability is calculated in quote currency - short: Liability is calculated in trading currency - :param interest: Interest that has not been deducted yet - :param position_assets: Total position assets - on-hold by pending order - - Total: The number of positive assets on the position (including margin). - long: with trading currency as position asset. - short: with quote currency as position asset. - - Est. liquidation price - long: (liability + interest)* (1 + maintenance margin ratio) * - (1 + taker fee rate) / position assets - short: (liability + interest)* (1 + maintenance margin ratio) * - (1 + taker fee rate) - - """ - if trading_mode == TradingMode.FUTURES and collateral == Collateral.ISOLATED: - if is_short: - return (liability + interest) * (1 + mm_ratio) * (1 + taker_fee_rate) - else: - return (liability + interest) * (1 + mm_ratio) * (1 + taker_fee_rate) / position_assets - else: - exception("okex", trading_mode, collateral) - - -def ftx( - open_rate: float, - is_short: bool, - leverage: float, - trading_mode: TradingMode, - collateral: Collateral - # ... -): - """ - # ! Not Implemented - Calculates the liquidation price on FTX - :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 trading_mode: SPOT, MARGIN, FUTURES, etc. - :param collateral: Either ISOLATED or CROSS - """ - if collateral == Collateral.CROSS: - exception("ftx", trading_mode, collateral) - - # If nothing was returned - exception("ftx", trading_mode, collateral) - - -def kraken( - open_rate: float, - is_short: bool, - leverage: float, - trading_mode: TradingMode, - collateral: Collateral - # ... -): - """ - # ! Not Implemented - MARGIN: - https://support.kraken.com/hc/en-us/articles/203325763-Margin-Call-Level-and-Margin-Liquidation-Level - - :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 trading_mode: SPOT, MARGIN, FUTURES, etc. - :param collateral: Either ISOLATED or CROSS - """ - - if collateral == Collateral.CROSS: - if trading_mode == TradingMode.MARGIN: - exception("kraken", trading_mode, collateral) - elif trading_mode == TradingMode.FUTURES: - exception("kraken", trading_mode, collateral) - - # If nothing was returned - exception("kraken", trading_mode, collateral) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index ad4a513b4..884afb11d 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -14,9 +14,9 @@ from sqlalchemy.pool import StaticPool from sqlalchemy.sql.schema import UniqueConstraint from freqtrade.constants import DATETIME_PRINT_FORMAT, NON_OPEN_EXCHANGE_STATES -from freqtrade.enums import Collateral, SellType, TradingMode +from freqtrade.enums import SellType, TradingMode from freqtrade.exceptions import DependencyException, OperationalException -from freqtrade.leverage import interest, liquidation_price +from freqtrade.leverage import interest from freqtrade.misc import safe_value_fallback from freqtrade.persistence.migrations import check_migrate @@ -364,52 +364,12 @@ class LocalTrade(): def set_isolated_liq( self, - isolated_liq: Optional[float] = None, - wallet_balance: Optional[float] = None, - current_price: Optional[float] = None, - maintenance_amt: Optional[float] = None, - mm_ratio: Optional[float] = None, + isolated_liq: float, ): """ Method you should use to set self.liquidation price. Assures stop_loss is not passed the liquidation price """ - if not isolated_liq: - if not wallet_balance or not current_price: - raise OperationalException( - "wallet balance must be passed to LocalTrade.set_isolated_liq when param" - "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( - exchange_name=self.exchange, - open_rate=self.open_rate, - is_short=self.is_short, - leverage=self.leverage, - trading_mode=self.trading_mode, - collateral=Collateral.ISOLATED, - mm_ex_1=0.0, - upnl_ex_1=0.0, - position=self.amount * current_price, - wallet_balance=self.amount / self.leverage, # TODO: Update for cross - maintenance_amt=maintenance_amt, - mm_ratio=mm_ratio, - - ) - if isolated_liq is None: - raise OperationalException( - "leverage/isolated_liq returned None. This exception should never happen" - ) - if self.stop_loss is not None: if self.is_short: self.stop_loss = min(self.stop_loss, isolated_liq) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 6e0c02e7c..22e92c9fd 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -26,6 +26,12 @@ from tests.conftest import get_mock_coro, get_patched_exchange, log_has, log_has # Make sure to always keep one exchange here which is NOT subclassed!! EXCHANGES = ['bittrex', 'binance', 'kraken', 'ftx', 'gateio'] +spot = TradingMode.SPOT +margin = TradingMode.MARGIN +futures = TradingMode.FUTURES + +cross = Collateral.CROSS +isolated = Collateral.ISOLATED def ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, @@ -3965,3 +3971,102 @@ def test__amount_to_contracts( assert result_size == param_size result_amount = exchange._contracts_to_amount(pair, param_size) assert result_amount == param_amount + + +@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), + # 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), +]) +def test_liquidation_price_is_none( + mocker, + default_conf, + exchange_name, + open_rate, + is_short, + leverage, + trading_mode, + 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, + ) 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, ' + 'mm_ratio, expected', + [ + ("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, + 0.0, 16300.000, 109.488, 32481.980, 0.025, 18778.73), + ("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, + -448192.89, 16300.000, 109.488, 32481.980, 0.025, 26316.89) + ]) +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 +): + 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, + maintenance_amt=maintenance_amt, + position=position, + mm_ratio=mm_ratio + ), 2), expected) diff --git a/tests/leverage/test_liquidation_price.py b/tests/leverage/test_liquidation_price.py deleted file mode 100644 index 4707a5680..000000000 --- a/tests/leverage/test_liquidation_price.py +++ /dev/null @@ -1,121 +0,0 @@ -from math import isclose - -import pytest - -from freqtrade.enums import Collateral, TradingMode -from freqtrade.leverage import liquidation_price - - -# from freqtrade.exceptions import OperationalException - -spot = TradingMode.SPOT -margin = TradingMode.MARGIN -futures = TradingMode.FUTURES - -cross = Collateral.CROSS -isolated = Collateral.ISOLATED - - -@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), - # 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), - # Kraken - ('kraken', "2.0", False, "3.0", spot, None), - ('kraken', "2.0", True, "3.0", spot, cross), - ('kraken', "2.0", False, "1.0", spot, isolated), - # FTX - ('ftx', "2.0", True, "3.0", spot, None), - ('ftx', "2.0", False, "3.0", spot, cross), - ('ftx', "2.0", False, "3.0", spot, isolated), -]) -def test_liquidation_price_is_none( - exchange_name, - open_rate, - is_short, - leverage, - trading_mode, - collateral -): - assert liquidation_price( - exchange_name, - open_rate, - is_short, - leverage, - trading_mode, - collateral, - 1535443.01, - 71200.81144, - -56354.57, - 135365.00, - 3683.979, - 0.10, - ) 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", False, "1.0", margin, isolated), - ('kraken', "2.0", False, "1.0", futures, isolated), - # FTX - ('ftx', "2.0", False, "3.0", margin, isolated), - ('ftx', "2.0", False, "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, ' - 'mm_ratio, expected', - [ - ("binance", False, 1, TradingMode.FUTURES, Collateral.ISOLATED, 1535443.01, 0.0, - 0.0, 135365.00, 3683.979, 1456.84, 0.10, 1114.78), - ("binance", False, 1, TradingMode.FUTURES, Collateral.ISOLATED, 1535443.01, 0.0, - 0.0, 16300.000, 109.488, 32481.980, 0.025, 18778.73), - ("binance", False, 1, TradingMode.FUTURES, Collateral.CROSS, 1535443.01, 71200.81144, - -56354.57, 135365.00, 3683.979, 1456.84, 0.10, 1153.26), - ("binance", False, 1, TradingMode.FUTURES, Collateral.CROSS, 1535443.01, 356512.508, - -448192.89, 16300.000, 109.488, 32481.980, 0.025, 26316.89) - ]) -def test_liquidation_price( - exchange_name, open_rate, is_short, leverage, trading_mode, collateral, wallet_balance, - mm_ex_1, upnl_ex_1, maintenance_amt, position, mm_ratio, expected -): - assert isclose(round(liquidation_price( - exchange_name=exchange_name, - 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, - maintenance_amt=maintenance_amt, - position=position, - mm_ratio=mm_ratio - ), 2), expected) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index affc86692..7c266b156 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -734,7 +734,6 @@ 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 (open_rate + (wallet_balance / position)) / (1 + (mm_ratio + taker_fee_rate)) (10 + (2 / 1)) / (1 + (0.01 + 0.0006)) = 11.87413417771621 From 5cf54bee4d97411af5238f8d564e845af0c706e8 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sat, 22 Jan 2022 20:24:41 -0600 Subject: [PATCH 37/53] removed excess decimals in test_binance --- tests/exchange/test_binance.py | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index 886c11980..6762fada7 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -176,21 +176,21 @@ def test_get_max_leverage_binance(default_conf, mocker, pair, nominal_value, max 'BNB/BUSD': [[0.0, 0.025, 0.0], [100000.0, 0.05, 2500.0], [500000.0, 0.1, 27500.0], - [1000000.0, 0.15, 77499.99999999999], + [1000000.0, 0.15, 77500.0], [2000000.0, 0.25, 277500.0], [5000000.0, 0.5, 1527500.0]], 'BNB/USDT': [[0.0, 0.0065, 0.0], - [10000.0, 0.01, 35.00000000000001], + [10000.0, 0.01, 35.0], [50000.0, 0.02, 535.0], - [250000.0, 0.05, 8035.000000000001], + [250000.0, 0.05, 8035.0], [1000000.0, 0.1, 58035.0], - [2000000.0, 0.125, 108034.99999999999], - [5000000.0, 0.15, 233034.99999999994], + [2000000.0, 0.125, 108035.0], + [5000000.0, 0.15, 233035.0], [10000000.0, 0.25, 1233035.0]], 'BTC/USDT': [[0.0, 0.004, 0.0], [50000.0, 0.005, 50.0], [250000.0, 0.01, 1300.0], - [1000000.0, 0.025, 16300.000000000002], + [1000000.0, 0.025, 16300.0], [5000000.0, 0.05, 141300.0], [20000000.0, 0.1, 1141300.0], [50000000.0, 0.125, 2391300.0], @@ -257,7 +257,7 @@ def test_fill_leverage_brackets_binance(default_conf, mocker): [5000.0, 0.025, 75.0], [25000.0, 0.05, 700.0], [100000.0, 0.1, 5700.0], - [250000.0, 0.125, 11949.999999999998], + [250000.0, 0.125, 11949.999999999998], [1000000.0, 0.5, 386950.0]] } @@ -398,7 +398,7 @@ def test__ccxt_config(default_conf, mocker, trading_mode, collateral, config): ("BNB/USDT", 100.0, 0.0065, 0), ("BTC/USDT", 170.30, 0.004, 0), ("BNB/BUSD", 999999.9, 0.1, 27500.0), - ("BNB/USDT", 5000000.0, 0.15, 233034.99999999994), + ("BNB/USDT", 5000000.0, 0.15, 233035.0), ("BTC/USDT", 300000000.1, 0.5, 99891300.0), ]) def test_get_maintenance_ratio_and_amt_binance( @@ -414,21 +414,21 @@ def test_get_maintenance_ratio_and_amt_binance( 'BNB/BUSD': [[0.0, 0.025, 0.0], [100000.0, 0.05, 2500.0], [500000.0, 0.1, 27500.0], - [1000000.0, 0.15, 77499.99999999999], + [1000000.0, 0.15, 77500.0], [2000000.0, 0.25, 277500.0], [5000000.0, 0.5, 1527500.0]], 'BNB/USDT': [[0.0, 0.0065, 0.0], - [10000.0, 0.01, 35.00000000000001], + [10000.0, 0.01, 35.0], [50000.0, 0.02, 535.0], - [250000.0, 0.05, 8035.000000000001], + [250000.0, 0.05, 8035.0], [1000000.0, 0.1, 58035.0], - [2000000.0, 0.125, 108034.99999999999], - [5000000.0, 0.15, 233034.99999999994], + [2000000.0, 0.125, 108035.0], + [5000000.0, 0.15, 233035.0], [10000000.0, 0.25, 1233035.0]], 'BTC/USDT': [[0.0, 0.004, 0.0], [50000.0, 0.005, 50.0], [250000.0, 0.01, 1300.0], - [1000000.0, 0.025, 16300.000000000002], + [1000000.0, 0.025, 16300.0], [5000000.0, 0.05, 141300.0], [20000000.0, 0.1, 1141300.0], [50000000.0, 0.125, 2391300.0], @@ -437,4 +437,5 @@ def test_get_maintenance_ratio_and_amt_binance( [300000000.0, 0.5, 99891300.0] ] } - assert exchange.get_maintenance_ratio_and_amt(pair, nominal_value) == (mm_ratio, amt) + (result_ratio, result_amt) = exchange.get_maintenance_ratio_and_amt(pair, nominal_value) + assert (round(result_ratio, 8), round(result_amt, 8)) == (mm_ratio, amt) From e91aaa7d64f79d61df5415c2a603419ddf3dfcce Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sat, 22 Jan 2022 20:34:42 -0600 Subject: [PATCH 38/53] removed isolated_liq= --- tests/test_persistence.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 9f9c2da19..5df3fc2bc 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -109,7 +109,7 @@ def test_set_stop_loss_isolated_liq(fee): leverage=2.0, trading_mode=margin ) - trade.set_isolated_liq(isolated_liq=0.09) + trade.set_isolated_liq(0.09) assert trade.isolated_liq == 0.09 assert trade.stop_loss == 0.09 assert trade.initial_stop_loss == 0.09 @@ -119,12 +119,12 @@ def test_set_stop_loss_isolated_liq(fee): assert trade.stop_loss == 0.1 assert trade.initial_stop_loss == 0.09 - trade.set_isolated_liq(isolated_liq=0.08) + trade.set_isolated_liq(0.08) assert trade.isolated_liq == 0.08 assert trade.stop_loss == 0.1 assert trade.initial_stop_loss == 0.09 - trade.set_isolated_liq(isolated_liq=0.11) + trade.set_isolated_liq(0.11) assert trade.isolated_liq == 0.11 assert trade.stop_loss == 0.11 assert trade.initial_stop_loss == 0.09 @@ -148,7 +148,7 @@ def test_set_stop_loss_isolated_liq(fee): trade.stop_loss = None trade.initial_stop_loss = None - trade.set_isolated_liq(isolated_liq=0.09) + trade.set_isolated_liq(0.09) assert trade.isolated_liq == 0.09 assert trade.stop_loss == 0.09 assert trade.initial_stop_loss == 0.09 @@ -158,12 +158,12 @@ def test_set_stop_loss_isolated_liq(fee): assert trade.stop_loss == 0.08 assert trade.initial_stop_loss == 0.09 - trade.set_isolated_liq(isolated_liq=0.1) + trade.set_isolated_liq(0.1) assert trade.isolated_liq == 0.1 assert trade.stop_loss == 0.08 assert trade.initial_stop_loss == 0.09 - trade.set_isolated_liq(isolated_liq=0.07) + trade.set_isolated_liq(0.07) assert trade.isolated_liq == 0.07 assert trade.stop_loss == 0.07 assert trade.initial_stop_loss == 0.09 @@ -1472,7 +1472,7 @@ def test_adjust_stop_loss_short(fee): assert trade.initial_stop_loss == 1.05 assert trade.initial_stop_loss_pct == 0.05 assert trade.stop_loss_pct == 0.1 - trade.set_isolated_liq(isolated_liq=0.63) + trade.set_isolated_liq(0.63) trade.adjust_stop_loss(0.59, -0.1) assert trade.stop_loss == 0.63 assert trade.isolated_liq == 0.63 @@ -1803,7 +1803,7 @@ def test_stoploss_reinitialization_short(default_conf, fee): assert trade_adj.initial_stop_loss == 1.04 assert trade_adj.initial_stop_loss_pct == 0.04 # Stoploss can't go above liquidation price - trade_adj.set_isolated_liq(isolated_liq=1.0) + trade_adj.set_isolated_liq(1.0) trade.adjust_stop_loss(0.97, -0.04) assert trade_adj.stop_loss == 1.0 assert trade_adj.stop_loss == 1.0 From 0b5c2e97b3c23200bcbda2f73fcd6d9a81093012 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Thu, 27 Jan 2022 22:50:49 -0600 Subject: [PATCH 39/53] exchange._get_maintenance_ratio_and_amount --- freqtrade/exchange/exchange.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index e277c0428..b74fbfe1d 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -2012,8 +2012,7 @@ class Exchange: """ :return: The maintenance margin ratio and maintenance amount """ - # TODO-lev: return the real amounts - return (0, 0.4) + raise OperationalException(self.name + ' does not support leverage futures trading') def liquidation_price( self, From fe037aa971cd7c92edc0bc472769d9ad4b99a927 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Fri, 28 Jan 2022 05:06:06 -0600 Subject: [PATCH 40/53] exchange.liquidation_price combined position and position_assets --- freqtrade/exchange/binance.py | 11 ++++------- freqtrade/exchange/exchange.py | 27 ++++++++++++--------------- freqtrade/exchange/gateio.py | 9 +++------ freqtrade/exchange/okex.py | 25 ++++++++++++------------- 4 files changed, 31 insertions(+), 41 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 55168aebf..01c22aee6 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -282,16 +282,15 @@ class Binance(Exchange): open_rate: float, # Entry price of position is_short: bool, leverage: float, - trading_mode: TradingMode, mm_ratio: float, + position: float, # Absolute value of position size + trading_mode: TradingMode, 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]: @@ -303,22 +302,20 @@ class Binance(Exchange): :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 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 - :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) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index b74fbfe1d..eb7b58a9c 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -2019,16 +2019,15 @@ class Exchange: open_rate: float, # Entry price of position is_short: bool, leverage: float, - trading_mode: TradingMode, mm_ratio: float, + position: float, # Absolute value of position size + trading_mode: TradingMode, collateral: Optional[Collateral] = Collateral.ISOLATED, 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]: @@ -2097,28 +2096,26 @@ class Exchange: taker_fee_rate, liability, interest, - position_assets, mm_ex_1, upnl_ex_1, ) def liquidation_price_helper( self, - open_rate: float, + open_rate: float, # Entry price of position is_short: bool, leverage: float, - trading_mode: TradingMode, mm_ratio: float, + position: float, # Absolute value of position size + trading_mode: TradingMode, collateral: Collateral, - maintenance_amt: Optional[float] = None, - position: Optional[float] = None, - wallet_balance: Optional[float] = None, - taker_fee_rate: Optional[float] = None, - liability: Optional[float] = None, - interest: Optional[float] = None, - position_assets: Optional[float] = None, - mm_ex_1: Optional[float] = 0.0, - upnl_ex_1: Optional[float] = 0.0, + maintenance_amt: Optional[float] = None, # (Binance) + 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) + mm_ex_1: Optional[float] = 0.0, # (Binance) Cross only + upnl_ex_1: Optional[float] = 0.0, # (Binance) Cross only ) -> Optional[float]: raise OperationalException(f"liquidation_price is not implemented for {self.name}") diff --git a/freqtrade/exchange/gateio.py b/freqtrade/exchange/gateio.py index e2dbf35c7..7cdd27175 100644 --- a/freqtrade/exchange/gateio.py +++ b/freqtrade/exchange/gateio.py @@ -57,16 +57,15 @@ class Gateio(Exchange): open_rate: float, # Entry price of position is_short: bool, leverage: float, - trading_mode: TradingMode, mm_ratio: float, + position: float, # Absolute value of position size + trading_mode: TradingMode, 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]: @@ -77,21 +76,19 @@ class Gateio(Exchange): :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 trading_mode: SPOT, MARGIN, FUTURES, etc. :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 maintenance_amt: # * Not required by Gateio :param wallet_balance: Cross-Margin Mode: crossWalletBalance Isolated-Margin Mode: isolatedWalletBalance - :param position: Absolute value of position size (in base currency) :param taker_fee_rate: # * Not required by Gateio :param liability: :param interest: - :param position_assets: :param mm_ex_1: :param upnl_ex_1: """ diff --git a/freqtrade/exchange/okex.py b/freqtrade/exchange/okex.py index f847f1180..0222f3b41 100644 --- a/freqtrade/exchange/okex.py +++ b/freqtrade/exchange/okex.py @@ -33,18 +33,17 @@ class Okex(Exchange): open_rate: float, # Entry price of position is_short: bool, leverage: float, - trading_mode: TradingMode, mm_ratio: float, + position: float, # Absolute value of position size + trading_mode: TradingMode, collateral: Collateral, - maintenance_amt: Optional[float] = None, # Not required - position: Optional[float] = None, # Not required - wallet_balance: Optional[float] = None, # Not required - taker_fee_rate: Optional[float] = None, # * required - liability: Optional[float] = None, # * required - interest: Optional[float] = None, # * required - position_assets: Optional[float] = None, # * required (Might be same as position) - mm_ex_1: Optional[float] = 0.0, # Not required - upnl_ex_1: Optional[float] = 0.0, # Not required + maintenance_amt: Optional[float] = None, # (Binance) + 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) + mm_ex_1: Optional[float] = 0.0, # (Binance) Cross only + upnl_ex_1: Optional[float] = 0.0, # (Binance) Cross only ) -> Optional[float]: """ PERPETUAL: https://www.okex.com/support/hc/en-us/articles/ @@ -54,22 +53,22 @@ class Okex(Exchange): :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: Okex: [assets in the position - (liability +interest) * mark price] / (maintenance margin + liquidation fee) + :param position: + Total position assets – on-hold by pending order + :param trading_mode: SPOT, MARGIN, FUTURES, etc. :param collateral: Either ISOLATED or CROSS :param maintenance_amt: # * Not required by Okex :param wallet_balance: # * Not required by Okex - :param position: # * Not required by Okex :param taker_fee_rate: :param liability: Initial liabilities + deducted interest • Long positions: Liability is calculated in quote currency. • Short positions: Liability is calculated in trading currency. :param interest: Interest that has not been deducted yet. - :param position_assets: Total position assets – on-hold by pending order :param mm_ex_1: # * Not required by Okex :param upnl_ex_1: # * Not required by Okex """ From 7f4894d68e82cf83099db0222b64e3fcec6925be Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Fri, 28 Jan 2022 05:47:19 -0600 Subject: [PATCH 41/53] okex.liquidation_price formula update --- freqtrade/exchange/binance.py | 4 --- freqtrade/exchange/exchange.py | 45 +++++++++++----------------------- freqtrade/exchange/gateio.py | 4 --- freqtrade/exchange/okex.py | 24 +++++------------- 4 files changed, 20 insertions(+), 57 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 01c22aee6..164e94060 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -289,8 +289,6 @@ class Binance(Exchange): maintenance_amt: Optional[float] = None, # (Binance) 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) mm_ex_1: Optional[float] = 0.0, # (Binance) Cross only upnl_ex_1: Optional[float] = 0.0, # (Binance) Cross only ) -> Optional[float]: @@ -314,8 +312,6 @@ class Binance(Exchange): # * Not required by Binance :param taker_fee_rate: - :param liability: - :param interest: # * Only required for Cross :param mm_ex_1: (TMM) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index eb7b58a9c..41e9f5e66 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -2026,8 +2026,6 @@ class Exchange: maintenance_amt: Optional[float] = None, # (Binance) 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) mm_ex_1: Optional[float] = 0.0, # (Binance) Cross only upnl_ex_1: Optional[float] = 0.0, # (Binance) Cross only ) -> Optional[float]: @@ -2036,12 +2034,12 @@ class Exchange: :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) 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 @@ -2051,21 +2049,10 @@ class Exchange: :param wallet_balance: (WB) Cross-Margin Mode: crossWalletBalance Isolated-Margin Mode: isolatedWalletBalance - :param position: Absolute value of position size (in base currency) # * Gateio & Okex :param taker_fee_rate: - # * Okex - :param liability: - Initial liabilities + deducted interest - • Long positions: Liability is calculated in quote currency. - • Short positions: Liability is calculated in trading currency. - :param interest: - Interest that has not been deducted yet. - :param position_assets: - Total position assets – on-hold by pending order - # * Cross only (Binance) :param mm_ex_1: (TMM) Cross-Margin Mode: Maintenance Margin of all other contracts, excluding Contract 1 @@ -2084,20 +2071,18 @@ class Exchange: ) return self.liquidation_price_helper( - open_rate, - is_short, - leverage, - trading_mode, - mm_ratio, - collateral, - maintenance_amt, - position, - wallet_balance, - taker_fee_rate, - liability, - interest, - mm_ex_1, - upnl_ex_1, + 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( @@ -2112,8 +2097,6 @@ class Exchange: maintenance_amt: Optional[float] = None, # (Binance) 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) mm_ex_1: Optional[float] = 0.0, # (Binance) Cross only upnl_ex_1: Optional[float] = 0.0, # (Binance) Cross only ) -> Optional[float]: diff --git a/freqtrade/exchange/gateio.py b/freqtrade/exchange/gateio.py index 7cdd27175..3428fa1cf 100644 --- a/freqtrade/exchange/gateio.py +++ b/freqtrade/exchange/gateio.py @@ -64,8 +64,6 @@ class Gateio(Exchange): maintenance_amt: Optional[float] = None, # (Binance) 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) mm_ex_1: Optional[float] = 0.0, # (Binance) Cross only upnl_ex_1: Optional[float] = 0.0, # (Binance) Cross only ) -> Optional[float]: @@ -87,8 +85,6 @@ class Gateio(Exchange): :param taker_fee_rate: # * Not required by Gateio - :param liability: - :param interest: :param mm_ex_1: :param upnl_ex_1: """ diff --git a/freqtrade/exchange/okex.py b/freqtrade/exchange/okex.py index 0222f3b41..b62d8fdf8 100644 --- a/freqtrade/exchange/okex.py +++ b/freqtrade/exchange/okex.py @@ -40,8 +40,6 @@ class Okex(Exchange): maintenance_amt: Optional[float] = None, # (Binance) 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) mm_ex_1: Optional[float] = 0.0, # (Binance) Cross only upnl_ex_1: Optional[float] = 0.0, # (Binance) Cross only ) -> Optional[float]: @@ -57,36 +55,26 @@ class Okex(Exchange): :param mm_ratio: Okex: [assets in the position - (liability +interest) * mark price] / (maintenance margin + liquidation fee) - :param position: - Total position assets – on-hold by pending order :param trading_mode: SPOT, MARGIN, FUTURES, etc. :param collateral: Either ISOLATED or CROSS :param maintenance_amt: # * Not required by Okex - :param wallet_balance: # * Not required by Okex + :param wallet_balance: # * margin_balance? :param taker_fee_rate: - :param liability: - Initial liabilities + deducted interest - • Long positions: Liability is calculated in quote currency. - • Short positions: Liability is calculated in trading currency. - :param interest: Interest that has not been deducted yet. :param mm_ex_1: # * Not required by Okex :param upnl_ex_1: # * Not required by Okex """ - if (not liability or not interest or not taker_fee_rate or not position_assets): + if (not taker_fee_rate): raise OperationalException( - "Parameters liability, interest, taker_fee_rate, position_assets" - "are required by Okex.liquidation_price" + "Parameter taker_fee_rate is required by Okex.liquidation_price" ) if trading_mode == TradingMode.FUTURES and collateral == Collateral.ISOLATED: + if is_short: - return (liability + interest) * (1 + mm_ratio) * (1 + taker_fee_rate) + return (margin_balance + (face_value * number_of_contracts * open_price)) / [face_value * number_of_contracts * (mm_ratio + taker_fee_rate + 1)] else: - return ( - (liability + interest) * (1 + mm_ratio) * (1 + taker_fee_rate) / - position_assets - ) + return (margin_balance - (face_value * number_of_contracts * open_price)) / [face_value * number_of_contracts * (mm_ratio + taker_fee_rate - 1)] else: raise OperationalException( f"Okex does not support {collateral.value} Mode {trading_mode.value} trading") From 88ce66650c91193b2b93727de65a4d117a8369ec Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Fri, 28 Jan 2022 21:55:17 -0600 Subject: [PATCH 42/53] Okex and Gateio liquidation_price formula are the same, moved liquidation_price to exchange.exchange class --- freqtrade/exchange/exchange.py | 78 +++++++++++++++++++++++++++------- freqtrade/exchange/gateio.py | 68 ----------------------------- freqtrade/exchange/okex.py | 51 ---------------------- 3 files changed, 62 insertions(+), 135 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 41e9f5e66..c7d837471 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -2085,22 +2085,68 @@ class Exchange: 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, - 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) - 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]: - raise OperationalException(f"liquidation_price is not implemented for {self.name}") + 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 + + :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) + else: + raise OperationalException( + f"{self.name} does not support {collateral.value} {trading_mode.value}") def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = None) -> bool: diff --git a/freqtrade/exchange/gateio.py b/freqtrade/exchange/gateio.py index 3428fa1cf..c62b6222d 100644 --- a/freqtrade/exchange/gateio.py +++ b/freqtrade/exchange/gateio.py @@ -51,71 +51,3 @@ class Gateio(Exchange): """ info = self.markets[pair]['info'] return (float(info['maintenance_rate']), None) - - 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 - trading_mode: TradingMode, - collateral: Collateral, - 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]: - """ - PERPETUAL: https://www.gate.io/help/futures/perpetual/22160/calculation-of-liquidation-price - - :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 maintenance_amt: # * Not required by Gateio - :param wallet_balance: - Cross-Margin Mode: crossWalletBalance - Isolated-Margin Mode: isolatedWalletBalance - :param taker_fee_rate: - - # * Not required by Gateio - :param mm_ex_1: - :param upnl_ex_1: - """ - 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 (not wallet_balance or not position or not taker_fee_rate): - raise OperationalException( - "Parameters wallet_balance, position, taker_fee_rate" - "are required by Gateio.liquidation_price" - ) - - if trading_mode == TradingMode.FUTURES and collateral == Collateral.ISOLATED: - # if is_inverse: - # # ! Not implemented - # 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) - else: - raise OperationalException( - f"Gateio does not support {collateral.value} Mode {trading_mode.value} trading ") diff --git a/freqtrade/exchange/okex.py b/freqtrade/exchange/okex.py index b62d8fdf8..b15e686d3 100644 --- a/freqtrade/exchange/okex.py +++ b/freqtrade/exchange/okex.py @@ -27,54 +27,3 @@ class Okex(Exchange): # (TradingMode.FUTURES, Collateral.CROSS), # (TradingMode.FUTURES, Collateral.ISOLATED) ] - - 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 - trading_mode: TradingMode, - collateral: Collateral, - 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]: - """ - PERPETUAL: 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 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: - Okex: [assets in the position - (liability +interest) * mark price] / - (maintenance margin + liquidation fee) - :param trading_mode: SPOT, MARGIN, FUTURES, etc. - :param collateral: Either ISOLATED or CROSS - :param maintenance_amt: # * Not required by Okex - :param wallet_balance: # * margin_balance? - :param taker_fee_rate: - :param mm_ex_1: # * Not required by Okex - :param upnl_ex_1: # * Not required by Okex - """ - - if (not taker_fee_rate): - raise OperationalException( - "Parameter taker_fee_rate is required by Okex.liquidation_price" - ) - - if trading_mode == TradingMode.FUTURES and collateral == Collateral.ISOLATED: - - if is_short: - return (margin_balance + (face_value * number_of_contracts * open_price)) / [face_value * number_of_contracts * (mm_ratio + taker_fee_rate + 1)] - else: - return (margin_balance - (face_value * number_of_contracts * open_price)) / [face_value * number_of_contracts * (mm_ratio + taker_fee_rate - 1)] - else: - raise OperationalException( - f"Okex does not support {collateral.value} Mode {trading_mode.value} trading") From d133a7c7892ac73920b58dec43593bad02a5c944 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Fri, 28 Jan 2022 21:57:34 -0600 Subject: [PATCH 43/53] added isolated, futures to okex trading_mode_collateral_pairs --- freqtrade/exchange/okex.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/exchange/okex.py b/freqtrade/exchange/okex.py index b15e686d3..a3623584c 100644 --- a/freqtrade/exchange/okex.py +++ b/freqtrade/exchange/okex.py @@ -25,5 +25,5 @@ class Okex(Exchange): # TradingMode.SPOT always supported and not required in this list # (TradingMode.MARGIN, Collateral.CROSS), # (TradingMode.FUTURES, Collateral.CROSS), - # (TradingMode.FUTURES, Collateral.ISOLATED) + (TradingMode.FUTURES, Collateral.ISOLATED) ] From ede9012fcce9a8904b3a147670d2e440fabe14a4 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Fri, 28 Jan 2022 22:03:16 -0600 Subject: [PATCH 44/53] removed TODO-levs about okex liquidation price --- freqtrade/exchange/ftx.py | 1 - freqtrade/freqtradebot.py | 6 ------ tests/exchange/test_exchange.py | 4 +--- tests/test_freqtradebot.py | 7 ++++--- 4 files changed, 5 insertions(+), 13 deletions(-) diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index e22281faf..e28c6bc45 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -27,7 +27,6 @@ class Ftx(Exchange): _supported_trading_mode_collateral_pairs: List[Tuple[TradingMode, Collateral]] = [ # TradingMode.SPOT always supported and not required in this list - # TODO-lev: Uncomment once supported # (TradingMode.MARGIN, Collateral.CROSS), # (TradingMode.FUTURES, Collateral.CROSS) ] diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 2911b8ea4..0485e9cc3 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -636,12 +636,6 @@ class FreqtradeBot(LoggingMixin): maintenance_amt=maintenance_amt, mm_ratio=mm_ratio, taker_fee_rate=taker_fee_rate - - # TODO-lev: Okex parameters - # liability: Optional[float]=None, - # interest: Optional[float]=None, - # position_assets: Optional[float]=None, # * Might be same as position - ) else: isolated_liq = self.exchange.get_liquidation_price(pair) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 22e92c9fd..7ba9bb2ee 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -3451,9 +3451,7 @@ def test_set_margin_mode(mocker, default_conf, collateral): ("binance", TradingMode.FUTURES, Collateral.ISOLATED, False), ("gateio", TradingMode.FUTURES, Collateral.ISOLATED, False), - - # ("okex", TradingMode.FUTURES, Collateral.ISOLATED, False), # TODO-lev: uncomment once impleme - ("okex", TradingMode.FUTURES, Collateral.ISOLATED, True), # TODO-lev: remove once implemented + ("okex", TradingMode.FUTURES, Collateral.ISOLATED, False), # * Remove once implemented ("binance", TradingMode.MARGIN, Collateral.CROSS, True), diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 7c266b156..e42a7ce88 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -712,13 +712,14 @@ def test_process_informative_pairs_added(default_conf_usdt, ticker_usdt, mocker) (True, 'spot', 'binance', None, None), (False, 'spot', 'gateio', None, None), (True, 'spot', 'gateio', None, None), + (False, 'spot', 'okex', None, None), + (True, 'spot', 'okex', None, None), (True, 'futures', 'binance', 'isolated', 11.89108910891089), (False, 'futures', 'binance', 'isolated', 8.070707070707071), (True, 'futures', 'gateio', 'isolated', 11.87413417771621), (False, 'futures', 'gateio', 'isolated', 8.085708510208207), - # TODO-lev: Okex - # (False, 'spot', 'okex', 'isolated', ...), - # (True, 'futures', 'okex', 'isolated', ...), + (True, 'futures', 'okex', 'isolated', 11.87413417771621), + (False, 'futures', 'okex', 'isolated', 8.085708510208207), ]) def test_execute_entry(mocker, default_conf_usdt, fee, limit_order, limit_order_open, is_short, trading_mode, From 143c37d36f032e7ebcb1934500c24f8d7f25c1f9 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sat, 29 Jan 2022 02:06:56 -0600 Subject: [PATCH 45/53] 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, From b8f4cebce7d9f31ec158ccb9414a71b0c8edab56 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sat, 29 Jan 2022 18:47:17 -0600 Subject: [PATCH 46/53] exchange.liquidation_price methods combined, dry_run check on exchange for liquidation price --- freqtrade/exchange/binance.py | 52 ++++++++-------------- freqtrade/exchange/exchange.py | 78 ++++++++++++++++++++++----------- freqtrade/exchange/okex.py | 1 + freqtrade/freqtradebot.py | 47 +++++++++----------- freqtrade/persistence/models.py | 5 +-- tests/conftest.py | 41 +++++++++++------ tests/exchange/test_exchange.py | 32 +++++++------- tests/test_freqtradebot.py | 8 ++-- 8 files changed, 140 insertions(+), 124 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 981126232..b4e1f3d57 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -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") diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index f3b4feb40..59f1c5d52 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -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: diff --git a/freqtrade/exchange/okex.py b/freqtrade/exchange/okex.py index b7babe6e9..86131faed 100644 --- a/freqtrade/exchange/okex.py +++ b/freqtrade/exchange/okex.py @@ -1,5 +1,6 @@ import logging from typing import Dict, List, Tuple + from freqtrade.enums import Collateral, TradingMode from freqtrade.exchange import Exchange diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index f39c97b3a..0f97804ce 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -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, diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 884afb11d..afee0725f 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -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 diff --git a/tests/conftest.py b/tests/conftest.py index 2bacb498e..0b534b7d0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -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, diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index cfdcaa596..08d0e255c 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -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) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index e42a7ce88..07187ceb1 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -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) From 2c1497b348f481c369027f23d07b1a12715fb6af Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sat, 29 Jan 2022 19:48:51 -0600 Subject: [PATCH 47/53] contracts_to_amount no longer in amount_to_precision --- freqtrade/exchange/exchange.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 59f1c5d52..c25a975e6 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -633,7 +633,6 @@ class Exchange: Re-implementation of ccxt internal methods - ensuring we can test the result is correct based on our definitions. """ - amount = self._amount_to_contracts(pair, amount) if self.markets[pair]['precision']['amount']: amount = float(decimal_to_precision(amount, rounding_mode=TRUNCATE, precision=self.markets[pair]['precision']['amount'], @@ -737,7 +736,7 @@ class Exchange: def create_dry_run_order(self, pair: str, ordertype: str, side: str, amount: float, rate: float, leverage: float, params: Dict = {}) -> Dict[str, Any]: order_id = f'dry_run_{side}_{datetime.now().timestamp()}' - _amount = self._contracts_to_amount(pair, self.amount_to_precision(pair, amount)) + _amount = self.amount_to_precision(pair, amount) dry_order: Dict[str, Any] = { 'id': order_id, 'symbol': pair, @@ -901,7 +900,7 @@ class Exchange: try: # Set the precision for amount and price(rate) as accepted by the exchange - amount = self.amount_to_precision(pair, amount) + amount = self.amount_to_precision(pair, self._amount_to_contracts(pair, amount)) needs_price = (ordertype != 'market' or self._api.options.get("createMarketBuyOrderRequiresPrice", False)) rate_for_order = self.price_to_precision(pair, rate) if needs_price else None From 43db4c34d11d4a0e5a25caad588108c9407dec2b Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sat, 29 Jan 2022 20:43:06 -0600 Subject: [PATCH 48/53] added okex back to unsupported exchanges --- freqtrade/exchange/okex.py | 2 +- tests/exchange/test_exchange.py | 7 ++++--- tests/test_freqtradebot.py | 4 ++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/freqtrade/exchange/okex.py b/freqtrade/exchange/okex.py index 86131faed..7e9348f0c 100644 --- a/freqtrade/exchange/okex.py +++ b/freqtrade/exchange/okex.py @@ -24,5 +24,5 @@ class Okex(Exchange): # TradingMode.SPOT always supported and not required in this list # (TradingMode.MARGIN, Collateral.CROSS), # (TradingMode.FUTURES, Collateral.CROSS), - (TradingMode.FUTURES, Collateral.ISOLATED) + # (TradingMode.FUTURES, Collateral.ISOLATED) ] diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 08d0e255c..aaeada8e2 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -242,8 +242,8 @@ def test_validate_order_time_in_force(default_conf, mocker, caplog): (2.34559, 4, 0.001, 1, 2.345, 'spot'), (2.9999, 4, 0.001, 1, 2.999, 'spot'), (2.9909, 4, 0.001, 1, 2.990, 'spot'), - (2.9909, 4, 0.005, 0.01, 299.09, 'futures'), - (2.9999, 4, 0.005, 10, 0.295, 'futures'), + (2.9909, 4, 0.005, 0.01, 2.99, 'futures'), + (2.9999, 4, 0.005, 10, 2.995, 'futures'), ]) def test_amount_to_precision( default_conf, @@ -3451,9 +3451,9 @@ def test_set_margin_mode(mocker, default_conf, collateral): ("binance", TradingMode.FUTURES, Collateral.ISOLATED, False), ("gateio", TradingMode.FUTURES, Collateral.ISOLATED, False), - ("okex", TradingMode.FUTURES, Collateral.ISOLATED, False), # * Remove once implemented + ("okex", TradingMode.FUTURES, Collateral.ISOLATED, True), ("binance", TradingMode.MARGIN, Collateral.CROSS, True), ("binance", TradingMode.FUTURES, Collateral.CROSS, True), ("kraken", TradingMode.MARGIN, Collateral.CROSS, True), @@ -3464,6 +3464,7 @@ def test_set_margin_mode(mocker, default_conf, collateral): ("gateio", TradingMode.FUTURES, Collateral.CROSS, True), # * Uncomment once implemented + # ("okex", TradingMode.FUTURES, Collateral.ISOLATED, False), # ("binance", TradingMode.MARGIN, Collateral.CROSS, False), # ("binance", TradingMode.FUTURES, Collateral.CROSS, False), # ("kraken", TradingMode.MARGIN, Collateral.CROSS, False), diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 07187ceb1..a2a3bebed 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -718,8 +718,8 @@ def test_process_informative_pairs_added(default_conf_usdt, ticker_usdt, mocker) (False, 'futures', 'binance', 'isolated', 8.070707070707071), (True, 'futures', 'gateio', 'isolated', 11.87413417771621), (False, 'futures', 'gateio', 'isolated', 8.085708510208207), - (True, 'futures', 'okex', 'isolated', 11.87413417771621), - (False, 'futures', 'okex', 'isolated', 8.085708510208207), + # (True, 'futures', 'okex', 'isolated', 11.87413417771621), + # (False, 'futures', 'okex', 'isolated', 8.085708510208207), ]) def test_execute_entry(mocker, default_conf_usdt, fee, limit_order, limit_order_open, is_short, trading_mode, From 8b9abd0051691b6c4dfa6f57c994333ee83fc7f8 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Mon, 31 Jan 2022 13:18:31 -0600 Subject: [PATCH 49/53] test_get_maintenance_ratio_and_amt_gateio removed commented test that returns None --- tests/exchange/test_gateio.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/tests/exchange/test_gateio.py b/tests/exchange/test_gateio.py index 96ae37598..a4d91c35c 100644 --- a/tests/exchange/test_gateio.py +++ b/tests/exchange/test_gateio.py @@ -34,7 +34,6 @@ def test_validate_order_types_gateio(default_conf, mocker): @pytest.mark.parametrize('pair,mm_ratio', [ ("ETH/USDT:USDT", 0.005), ("ADA/USDT:USDT", 0.003), - # ("DOGE/USDT:USDT", None), ]) def test_get_maintenance_ratio_and_amt_gateio(default_conf, mocker, pair, mm_ratio): api_mock = MagicMock() @@ -61,15 +60,6 @@ def test_get_maintenance_ratio_and_amt_gateio(default_conf, mocker, pair, mm_rat 'id': 'ADA_USDT', 'symbol': 'ADA/USDT:USDT', }, - # 'DOGE/USDT:USDT': { - # 'taker': 0.0000075, - # 'maker': -0.0000025, - # 'info': { - # 'nonmaintenance_rate': '0.003', - # }, - # 'id': 'DOGE_USDT', - # 'symbol': 'DOGE/USDT:USDT', - # } } ) ) From 430051275abfb4f43223a1c1867fc1c6a35adb96 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Mon, 31 Jan 2022 13:21:27 -0600 Subject: [PATCH 50/53] freqtradebot.leverage_prep moved wallet_balance to a variable --- freqtrade/freqtradebot.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 0f97804ce..ad5bc0fb6 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -618,12 +618,13 @@ class FreqtradeBot(LoggingMixin): self.collateral_type == Collateral.ISOLATED and self.trading_mode == TradingMode.FUTURES ): + wallet_balance = (amount * open_rate)/leverage 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 + wallet_balance=wallet_balance, mm_ex_1=0.0, upnl_ex_1=0.0, ) @@ -1176,8 +1177,8 @@ class FreqtradeBot(LoggingMixin): max_timeouts = self.config.get('unfilledtimeout', {}).get('exit_timeout_count', 0) if not_closed and (fully_cancelled or self.strategy.ft_check_timed_out( - time_method, trade, order, datetime.now(timezone.utc)) - ): + time_method, trade, order, datetime.now(timezone.utc)) + ): if is_entering: self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['TIMEOUT']) else: From 6c4325b7a2e9e644ee8a38c2c7f63842de046091 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Mon, 31 Jan 2022 13:44:57 -0600 Subject: [PATCH 51/53] confest markets removed futures values from ETH/USDT --- tests/conftest.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 0b534b7d0..de1f44e89 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -817,10 +817,10 @@ def get_markets(): 'symbol': 'ETH/USDT', 'base': 'ETH', 'quote': 'USDT', - 'settle': 'USDT', + 'settle': None, 'baseId': 'ETH', 'quoteId': 'USDT', - 'settleId': 'USDT', + 'settleId': None, 'type': 'spot', 'spot': True, 'margin': True, @@ -828,14 +828,14 @@ def get_markets(): 'future': True, 'option': False, 'active': True, - 'contract': True, - 'linear': True, - 'inverse': False, + 'contract': None, + 'linear': None, + 'inverse': None, 'taker': 0.0006, 'maker': 0.0002, - 'contractSize': 1, - 'expiry': 1680220800000, - 'expiryDateTime': '2023-03-31T00:00:00.000Z', + 'contractSize': None, + 'expiry': None, + 'expiryDateTime': None, 'strike': None, 'optionType': None, 'precision': { From 9de63412c19ce1155125f53b47e0a485f60ffacc Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Mon, 31 Jan 2022 13:49:06 -0600 Subject: [PATCH 52/53] exchange.get_liquidation_price arguments are not optional --- freqtrade/exchange/exchange.py | 18 ++++----------- tests/exchange/test_exchange.py | 41 ++++++++++++++++++++------------- 2 files changed, 29 insertions(+), 30 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index c25a975e6..3ba791861 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1987,10 +1987,10 @@ class Exchange: 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 + open_rate: float, # Entry price of position + is_short: bool, + position: float, # Absolute value of position size + wallet_balance: float, # Or margin balance mm_ex_1: float = 0.0, # (Binance) Cross only upnl_ex_1: float = 0.0, # (Binance) Cross only ): @@ -2007,16 +2007,6 @@ class Exchange: 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 ( - 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, diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index aaeada8e2..ba96e45f3 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -3631,7 +3631,13 @@ def test_get_liquidation_price(mocker, default_conf): default_conf['collateral'] = 'isolated' exchange = get_patched_exchange(mocker, default_conf, api_mock) - liq_price = exchange.get_liquidation_price('NEAR/USDT:USDT') + liq_price = exchange.get_liquidation_price( + pair='NEAR/USDT:USDT', + open_rate=0.0, + is_short=False, + position=0.0, + wallet_balance=0.0, + ) assert liq_price == 17.47 ccxt_exceptionhandlers( @@ -3641,7 +3647,11 @@ def test_get_liquidation_price(mocker, default_conf): "binance", "get_liquidation_price", "fetch_positions", - pair="XRP/USDT" + pair="XRP/USDT", + open_rate=0.0, + is_short=False, + position=0.0, + wallet_balance=0.0, ) @@ -3974,15 +3984,15 @@ def test__amount_to_contracts( assert result_amount == param_amount -@pytest.mark.parametrize('exchange_name,open_rate,is_short,leverage,trading_mode,collateral', [ +@pytest.mark.parametrize('exchange_name,open_rate,is_short,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, 'spot', None), + ('bittrex', 2.0, False, 'spot', 'cross'), + ('bittrex', 2.0, True, '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, 'spot', None), + ('binance', 2.0, False, 'spot', 'cross'), + ('binance', 2.0, True, 'spot', 'isolated'), ]) def test_liquidation_price_is_none( mocker, @@ -3990,7 +4000,6 @@ def test_liquidation_price_is_none( exchange_name, open_rate, is_short, - leverage, trading_mode, collateral ): @@ -4009,21 +4018,21 @@ def test_liquidation_price_is_none( @pytest.mark.parametrize( - 'exchange_name, is_short, leverage, trading_mode, collateral, wallet_balance, ' + 'exchange_name, is_short, trading_mode, collateral, wallet_balance, ' '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, '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, '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, '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, 'futures', 'cross', 1535443.01, 356512.508, -448192.89, 16300.000, 109.488, 32481.980, 0.025, 26316.89) ]) 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, trading_mode, collateral, wallet_balance, mm_ex_1, upnl_ex_1, maintenance_amt, position, mm_ratio, expected ): default_conf['trading_mode'] = trading_mode From ed320bb2ace412d3b8039c3fc086aaa750167272 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Mon, 31 Jan 2022 13:51:26 -0600 Subject: [PATCH 53/53] exchange.get_liquidation_price check length of positions --- freqtrade/exchange/exchange.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 3ba791861..837a390ed 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1993,7 +1993,7 @@ class Exchange: wallet_balance: float, # Or margin balance mm_ex_1: float = 0.0, # (Binance) Cross only upnl_ex_1: float = 0.0, # (Binance) Cross only - ): + ) -> Optional[float]: """ 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") @@ -2020,8 +2020,11 @@ class Exchange: try: positions = self._api.fetch_positions([pair]) - pos = positions[0] - return pos['liquidationPrice'] + if len(positions) > 0: + pos = positions[0] + return pos['liquidationPrice'] + else: + return None except ccxt.DDoSProtection as e: raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: