From 04f254b885cbe06e1a1c1b536ab6f334188cc5bd Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Fri, 6 Aug 2021 01:15:18 -0600 Subject: [PATCH] Added liquidation_price function --- freqtrade/leverage/__init__.py | 1 + freqtrade/leverage/liquidation_price.py | 106 ++++++++++++++++++++++++ freqtrade/persistence/models.py | 15 +++- tests/leverage/test_leverage.py | 89 ++++++++++++++++++++ tests/test_persistence.py | 24 +++--- 5 files changed, 220 insertions(+), 15 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 9186b160e..c918adc47 100644 --- a/freqtrade/leverage/__init__.py +++ b/freqtrade/leverage/__init__.py @@ -1 +1,2 @@ # flake8: noqa: F401 +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..2aeba6b78 --- /dev/null +++ b/freqtrade/leverage/liquidation_price.py @@ -0,0 +1,106 @@ +from freqtrade.enums import Collateral, TradingMode +from freqtrade.exceptions import OperationalException + + +def liquidation_price( + exchange_name: str, + trading_mode: TradingMode, + ** k +): + + leverage_exchanges = [ + 'binance', + 'kraken', + 'ftx' + ] + if trading_mode == TradingMode.SPOT or exchange_name.lower() not in leverage_exchanges: + return None + + collateral: Collateral = k['collateral'] + + if exchange_name.lower() == "binance": + # TODO-lev: Get more variables from **k and pass them to binance + return binance(trading_mode, collateral) + elif exchange_name.lower() == "kraken": + # TODO-lev: Get more variables from **k and pass them to kraken + return kraken(trading_mode, collateral) + elif exchange_name.lower() == "ftx": + return ftx(trading_mode, collateral) + return + + +def exception( + exchange_name: 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_name} does not support {collateral.value} {trading_mode.value} trading") + + +def binance(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(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(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 d09c5ed68..2875f52a8 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -16,6 +16,7 @@ from sqlalchemy.sql.schema import UniqueConstraint from freqtrade.constants import DATETIME_PRINT_FORMAT from freqtrade.enums import InterestMode, SellType from freqtrade.exceptions import DependencyException, OperationalException +from freqtrade.leverage import liquidation_price from freqtrade.misc import safe_value_fallback from freqtrade.persistence.migrations import check_migrate @@ -236,7 +237,7 @@ class LocalTrade(): close_rate_requested: Optional[float] = None close_profit: Optional[float] = None close_profit_abs: Optional[float] = None - stake_amount: float = 0.0 # TODO: This should probably be computed + stake_amount: float = 0.0 amount: float = 0.0 amount_requested: Optional[float] = None open_date: datetime @@ -316,7 +317,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() def _set_stop_loss(self, stop_loss: float, percent: float): @@ -342,11 +343,19 @@ 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, **k): """ Method you should use to set self.liquidation price. Assures stop_loss is not passed the liquidation price """ + if k['isolated_liq']: + isolated_liq: float = k['isolated_liq'] + else: + isolated_liq: float = liquidation_price( + exchange=self.exchange_name, + **k + ) + 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 16469f6fc..e99a1c38b 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -91,7 +91,7 @@ def test_enter_exit_side(fee): @pytest.mark.usefixtures("init_persistence") -def test__set_stop_loss_isolated_liq(fee): +def test_set_stop_loss_isolated_liq(fee): trade = Trade( id=2, pair='ADA/USDT', @@ -106,7 +106,7 @@ def test__set_stop_loss_isolated_liq(fee): is_short=False, leverage=2.0 ) - 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 @@ -116,12 +116,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 @@ -145,7 +145,7 @@ def test__set_stop_loss_isolated_liq(fee): trade.stop_loss = None trade.initial_stop_loss = None - 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 @@ -155,12 +155,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(0.1) + trade.set_isolated_liq(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(0.07) + trade.set_isolated_liq(isolated_liq=0.07) assert trade.isolated_liq == 0.07 assert trade.stop_loss == 0.07 assert trade.initial_stop_loss == 0.09 @@ -234,7 +234,7 @@ def test_interest(market_buy_order_usdt, fee): open_date=datetime.utcnow() - timedelta(hours=0, minutes=10), fee_open=fee.return_value, fee_close=fee.return_value, - exchange='kraken', + exchange='binance', leverage=3.0, interest_rate=0.0005, interest_mode=InterestMode.HOURSPERDAY @@ -506,7 +506,7 @@ def test_update_limit_order(limit_buy_order_usdt, limit_sell_order_usdt, fee, ca open_date=arrow.utcnow().datetime, fee_open=fee.return_value, fee_close=fee.return_value, - exchange='binance', + exchange='binance' ) assert trade.open_order_id is None assert trade.close_profit is None @@ -1575,11 +1575,11 @@ def test_adjust_stop_loss_short(fee): assert trade.initial_stop_loss_pct == 0.05 # Initial is true but stop_loss set - so doesn't do anything trade.adjust_stop_loss(0.3, -0.1, True) - assert round(trade.stop_loss, 8) == 0.66 # TODO-mg: What is this test? + assert round(trade.stop_loss, 8) == 0.66 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 @@ -1899,7 +1899,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