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