From b343d38112b86585ce3e98d30fe7a120cae78564 Mon Sep 17 00:00:00 2001 From: Arunavo Ray Date: Wed, 15 Sep 2021 06:55:44 +0530 Subject: [PATCH 01/22] =?UTF-8?q?Added=20Formulas=20to=20Calculate=20Liqui?= =?UTF-8?q?dation=20Price=20of=20Binance=20USD=E2=93=88-M=20Futures?= 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 | 236 ++++++++++++++++++++++++ 3 files changed, 247 insertions(+) create mode 100644 freqtrade/enums/marginmode.py create mode 100644 freqtrade/leverage/liquidation_price.py diff --git a/freqtrade/enums/__init__.py b/freqtrade/enums/__init__.py index 692a7fcb6..610b5cf43 100644 --- a/freqtrade/enums/__init__.py +++ b/freqtrade/enums/__init__.py @@ -7,3 +7,4 @@ from freqtrade.enums.selltype import SellType from freqtrade.enums.signaltype import 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 new file mode 100644 index 000000000..00e6c904c --- /dev/null +++ b/freqtrade/leverage/liquidation_price.py @@ -0,0 +1,236 @@ +from typing import Optional + +from freqtrade.enums import Collateral, TradingMode, MarginMode +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], + margin_mode: Optional[MarginMode] +) -> Optional[float]: + 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 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": + return ftx(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, + margin_mode: Optional[MarginMode] = None +): + """ + 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 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 ") + + +def binance( + open_rate: float, + is_short: bool, + leverage: float, + margin_mode: MarginMode, + trading_mode: TradingMode, + collateral: Collateral, + **kwargs +): + 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 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) + + 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, margin_mode) + + +def kraken( + open_rate: float, + is_short: bool, + leverage: float, + 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 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 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 + """ + 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) \ No newline at end of file From f0aa76d799d0b650d5ea909e634c0a446ffe3d2b Mon Sep 17 00:00:00 2001 From: Arunavo Ray Date: Wed, 15 Sep 2021 07:06:05 +0530 Subject: [PATCH 02/22] Revert "Added Ftx interest rate calculation" This reverts commit d07c7f7f275af5169b4ba3ac6ca25974c687998f. --- freqtrade/leverage/interest.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/freqtrade/leverage/interest.py b/freqtrade/leverage/interest.py index c687c8b5b..aacbb3532 100644 --- a/freqtrade/leverage/interest.py +++ b/freqtrade/leverage/interest.py @@ -20,7 +20,7 @@ def interest( :param exchange_name: The exchanged being trading on :param borrowed: The amount of currency being borrowed - :param rate: The rate of interest (i.e daily interest rate) + :param rate: The rate of interest :param hours: The time in hours that the currency has been borrowed for Raises: @@ -36,8 +36,7 @@ def interest( # Rounded based on https://kraken-fees-calculator.github.io/ return borrowed * rate * (one+ceil(hours/four)) elif exchange_name == "ftx": - # As Explained under #Interest rates section in - # https://help.ftx.com/hc/en-us/articles/360053007671-Spot-Margin-Trading-Explainer - return borrowed * rate * ceil(hours)/twenty_four + # TODO-lev: Add FTX interest formula + raise OperationalException(f"Leverage not available on {exchange_name} with freqtrade") else: - raise OperationalException(f"Leverage not available on {exchange_name} with freqtrade") \ No newline at end of file + raise OperationalException(f"Leverage not available on {exchange_name} with freqtrade") From 13331e4dc7afe0c52860efb9541d469993e02f6c Mon Sep 17 00:00:00 2001 From: Arunavo Ray Date: Fri, 17 Sep 2021 08:17:15 +0530 Subject: [PATCH 03/22] Converted kwargs to params --- freqtrade/leverage/liquidation_price.py | 105 +++++++++++------------- 1 file changed, 49 insertions(+), 56 deletions(-) diff --git a/freqtrade/leverage/liquidation_price.py b/freqtrade/leverage/liquidation_price.py index 00e6c904c..2379875ac 100644 --- a/freqtrade/leverage/liquidation_price.py +++ b/freqtrade/leverage/liquidation_price.py @@ -65,7 +65,21 @@ def binance( margin_mode: MarginMode, trading_mode: TradingMode, collateral: Collateral, - **kwargs + 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, ): r""" Calculates the liquidation price on Binance @@ -76,79 +90,58 @@ def binance( :param trading_mode: spot, margin, futures :param collateral: cross, isolated - :param \**kwargs: - See below + :param wallet_balance: Wallet Balance is crossWalletBalance in Cross-Margin Mode. + Wallet Balance is isolatedWalletBalance in Isolated Margin Mode - :Keyword Arguments: - * *wallet_balance* (``float``) -- - 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 - * *maintenance_margin_ex_1* (``float``) -- - 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 - * *unrealized_pnl_ex_1* (``float``) -- - 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) - * *maintenance_amount_both* (``float``) -- - Maintenance Amount of BOTH position (one-way mode) + :param maintenance_amount_long: Maintenance Amount of LONG position (hedge mode) - * *maintenance_amount_long* (``float``) -- - Maintenance Amount of LONG position (hedge mode) + :param maintenance_amount_short: Maintenance Amount of SHORT position (hedge mode) - * *maintenance_amount_short* (``float``) -- - 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 - * *side_1_both* (``int``) -- - 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) - * *position_1_both* (``float``) -- - Absolute value of BOTH position size (one-way mode) + :param entry_price_1_both: Entry Price of BOTH position (one-way mode) - * *entry_price_1_both* (``float``) -- - Entry Price of BOTH position (one-way mode) + :param position_1_long: Absolute value of LONG position size (hedge mode) - * *position_1_long* (``float``) -- - Absolute value of LONG position size (hedge mode) + :param entry_price_1_long: Entry Price of LONG position (hedge mode) - * *entry_price_1_long* (``float``) -- - Entry Price of LONG position (hedge mode) + :param position_1_short: Absolute value of SHORT position size (hedge mode) - * *position_1_short* (``float``) -- - Absolute value of SHORT position size (hedge mode) + :param entry_price_1_short: Entry Price of SHORT position (hedge mode) - * *entry_price_1_short* (``float``) -- - Entry Price of SHORT position (hedge mode) + :param maintenance_margin_rate_both: Maintenance margin rate of BOTH position (one-way mode) - * *maintenance_margin_rate_both* (``float``) -- - Maintenance margin rate of BOTH position (one-way mode) + :param maintenance_margin_rate_long: Maintenance margin rate of LONG position (hedge 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) + :param maintenance_margin_rate_short: 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") + 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(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") + 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 6a81730a74847edd6e431fe6ede299ac0639174e Mon Sep 17 00:00:00 2001 From: Arunavo Ray Date: Sun, 19 Sep 2021 10:18:28 +0530 Subject: [PATCH 04/22] Binance Liquidation Price Hedge-Mode Removed --- freqtrade/enums/__init__.py | 1 - freqtrade/enums/marginmode.py | 10 --- freqtrade/leverage/liquidation_price.py | 97 ++++++++----------------- 3 files changed, 29 insertions(+), 79 deletions(-) delete mode 100644 freqtrade/enums/marginmode.py diff --git a/freqtrade/enums/__init__.py b/freqtrade/enums/__init__.py index 610b5cf43..692a7fcb6 100644 --- a/freqtrade/enums/__init__.py +++ b/freqtrade/enums/__init__.py @@ -7,4 +7,3 @@ from freqtrade.enums.selltype import SellType from freqtrade.enums.signaltype import 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 2379875ac..8a9063a81 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 @@ -11,7 +11,13 @@ def liquidation_price( leverage: float, trading_mode: TradingMode, collateral: Optional[Collateral], - margin_mode: Optional[MarginMode] + 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: return None @@ -23,11 +29,16 @@ def liquidation_price( ) if exchange_name.lower() == "binance": - if not margin_mode: + 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"Parameter margin_mode is required by liquidation_price when exchange is {trading_mode}") + 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, margin_mode, trading_mode, collateral) + 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": @@ -41,52 +52,37 @@ def exception( exchange: str, trading_mode: TradingMode, collateral: Collateral, - margin_mode: Optional[MarginMode] = None ): """ 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 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} Mode {trading_mode.value} trading ") def binance( open_rate: float, is_short: bool, leverage: float, - margin_mode: MarginMode, trading_mode: TradingMode, 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, ): 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 trading_mode: spot, margin, futures :param collateral: cross, isolated @@ -101,80 +97,45 @@ 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, margin_mode) + 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 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) + # 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) + # 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, margin_mode) + exception("binance", trading_mode, collateral) def kraken( @@ -200,10 +161,10 @@ 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) # If nothing was returned - exception("kraken", trading_mode, collateral) + exception("kraken", trading_mode, collateral) def ftx( @@ -223,7 +184,7 @@ def ftx( """ if collateral == Collateral.CROSS: # TODO-lev: Additional arguments, fill in formulas - exception("ftx", trading_mode, collateral) + exception("ftx", trading_mode, collateral) # If nothing was returned - exception("ftx", trading_mode, collateral) \ No newline at end of file + exception("ftx", trading_mode, collateral) From 98d2d2f485f9605b5aa8eb7dd674b3bb3150f588 Mon Sep 17 00:00:00 2001 From: Arunavo Ray Date: Sun, 19 Sep 2021 11:16:41 +0530 Subject: [PATCH 05/22] Added Tests for Binance Liquidation price --- freqtrade/leverage/__init__.py | 1 + freqtrade/leverage/liquidation_price.py | 4 +-- tests/leverage/test_liquidation_price.py | 40 ++++++++++++++++++++++++ 3 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 tests/leverage/test_liquidation_price.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 index 8a9063a81..24b016ac9 100644 --- a/freqtrade/leverage/liquidation_price.py +++ b/freqtrade/leverage/liquidation_price.py @@ -123,7 +123,7 @@ 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)) / ( + 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: @@ -131,7 +131,7 @@ 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)) / ( + 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 diff --git a/tests/leverage/test_liquidation_price.py b/tests/leverage/test_liquidation_price.py new file mode 100644 index 000000000..f3fd54644 --- /dev/null +++ b/tests/leverage/test_liquidation_price.py @@ -0,0 +1,40 @@ +from math import isclose + +import pytest + +from freqtrade.enums import TradingMode, Collateral +from freqtrade.leverage import liquidation_price + + +@pytest.mark.parametrize( + 'exchange_name, 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, ' + 'expected', + [ + ("binance", 0.0, False, 1, TradingMode.FUTURES, Collateral.ISOLATED, 1535443.01, 71200.81144, -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, + maintenance_margin_ex_1, unrealized_pnl_ex_1, maintenance_amount_both, position_1_both, + entry_price_1_both, maintenance_margin_rate_both, 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, + maintenance_margin_ex_1=maintenance_margin_ex_1, + unrealized_pnl_ex_1=unrealized_pnl_ex_1, + maintenance_amount_both=maintenance_amount_both, + position_1_both=position_1_both, + entry_price_1_both=entry_price_1_both, + maintenance_margin_rate_both=maintenance_margin_rate_both + ), 2), expected) From e5a9e118990609b925524d640c2a288921c8a17f Mon Sep 17 00:00:00 2001 From: Arunavo Ray Date: Sun, 19 Sep 2021 11:22:22 +0530 Subject: [PATCH 06/22] Removed `both` from parameters in liquidation price --- freqtrade/leverage/liquidation_price.py | 54 +++++++++---------- .../{test_leverage.py => test_interest.py} | 0 tests/leverage/test_liquidation_price.py | 14 ++--- 3 files changed, 34 insertions(+), 34 deletions(-) rename tests/leverage/{test_leverage.py => test_interest.py} (100%) diff --git a/freqtrade/leverage/liquidation_price.py b/freqtrade/leverage/liquidation_price.py index 24b016ac9..025a64846 100644 --- a/freqtrade/leverage/liquidation_price.py +++ b/freqtrade/leverage/liquidation_price.py @@ -14,10 +14,10 @@ def liquidation_price( 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] + maintenance_amount: Optional[float], + position_1: Optional[float], + entry_price_1: Optional[float], + maintenance_margin_rate: Optional[float] ) -> Optional[float]: if trading_mode == TradingMode.SPOT: return None @@ -29,16 +29,16 @@ 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 maintenance_margin_ex_1 or not unrealized_pnl_ex_1 or not maintenance_amount \ + or not position_1 or not entry_price_1 or not maintenance_margin_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"Parameters wallet_balance, maintenance_margin_ex_1, unrealized_pnl_ex_1, maintenance_amount, " + f"position_1, entry_price_1, maintenance_margin_rate 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) + unrealized_pnl_ex_1, maintenance_amount, position_1, entry_price_1, + maintenance_margin_rate) elif exchange_name.lower() == "kraken": return kraken(open_rate, is_short, leverage, trading_mode, collateral) elif exchange_name.lower() == "ftx": @@ -73,10 +73,10 @@ def binance( 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, + maintenance_amount: float, + position_1: float, + entry_price_1: float, + maintenance_margin_rate: float, ): r""" Calculates the liquidation price on Binance @@ -95,24 +95,24 @@ def binance( :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: Maintenance Amount of position (one-way mode) - :param position_1_both: Absolute value of BOTH position size (one-way mode) + :param position_1: Absolute value of position size (one-way mode) - :param entry_price_1_both: Entry Price of BOTH position (one-way mode) + :param entry_price_1: Entry Price of position (one-way mode) - :param maintenance_margin_rate_both: Maintenance margin rate of BOTH position (one-way mode) + :param maintenance_margin_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 + cum_b = maintenance_amount + side_1 = -1 if is_short else 1 + position_1 = abs(position_1) + ep1 = entry_price_1 + mmr_b = maintenance_margin_rate if trading_mode == TradingMode.MARGIN and collateral == Collateral.CROSS: # TODO-lev: perform a calculation based on this formula @@ -123,16 +123,16 @@ 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_1 * ep1) / ( + position_1 * mmr_b - side_1 * position_1) 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 - 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_1 * ep1) / ( + position_1 * mmr_b - side_1 * position_1) # If nothing was returned exception("binance", trading_mode, collateral) diff --git a/tests/leverage/test_leverage.py b/tests/leverage/test_interest.py similarity index 100% rename from tests/leverage/test_leverage.py rename to tests/leverage/test_interest.py diff --git a/tests/leverage/test_liquidation_price.py b/tests/leverage/test_liquidation_price.py index f3fd54644..216d0609a 100644 --- a/tests/leverage/test_liquidation_price.py +++ b/tests/leverage/test_liquidation_price.py @@ -8,7 +8,7 @@ from freqtrade.leverage import liquidation_price @pytest.mark.parametrize( 'exchange_name, 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, ' + 'unrealized_pnl_ex_1, maintenance_amount, position_1, entry_price_1, maintenance_margin_rate, ' 'expected', [ ("binance", 0.0, False, 1, TradingMode.FUTURES, Collateral.ISOLATED, 1535443.01, 71200.81144, -56354.57, @@ -21,8 +21,8 @@ from freqtrade.leverage import liquidation_price 109.488, 32481.980, 0.025, 26316.89) ]) def test_liquidation_price(exchange_name, 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, expected): + maintenance_margin_ex_1, unrealized_pnl_ex_1, maintenance_amount, position_1, + entry_price_1, maintenance_margin_rate, expected): assert isclose(round(liquidation_price( exchange_name=exchange_name, open_rate=open_rate, @@ -33,8 +33,8 @@ def test_liquidation_price(exchange_name, open_rate, is_short, leverage, trading wallet_balance=wallet_balance, maintenance_margin_ex_1=maintenance_margin_ex_1, unrealized_pnl_ex_1=unrealized_pnl_ex_1, - maintenance_amount_both=maintenance_amount_both, - position_1_both=position_1_both, - entry_price_1_both=entry_price_1_both, - maintenance_margin_rate_both=maintenance_margin_rate_both + maintenance_amount=maintenance_amount, + position_1=position_1, + entry_price_1=entry_price_1, + maintenance_margin_rate=maintenance_margin_rate ), 2), expected) From 544b88f026490af59594b5713d96cad9d784e188 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sun, 19 Sep 2021 00:05:39 -0600 Subject: [PATCH 07/22] added one test for binance isolated liq --- freqtrade/enums/__init__.py | 1 - freqtrade/leverage/liquidation_price.py | 29 ++++++++++--- tests/leverage/test_liquidation_price.py | 55 ++++++++++++++++-------- 3 files changed, 60 insertions(+), 25 deletions(-) diff --git a/freqtrade/enums/__init__.py b/freqtrade/enums/__init__.py index 610b5cf43..692a7fcb6 100644 --- a/freqtrade/enums/__init__.py +++ b/freqtrade/enums/__init__.py @@ -7,4 +7,3 @@ from freqtrade.enums.selltype import SellType from freqtrade.enums.signaltype import SignalTagType, SignalType from freqtrade.enums.state import State from freqtrade.enums.tradingmode import TradingMode -from freqtrade.enums.marginmode import MarginMode diff --git a/freqtrade/leverage/liquidation_price.py b/freqtrade/leverage/liquidation_price.py index 8a9063a81..2eb660394 100644 --- a/freqtrade/leverage/liquidation_price.py +++ b/freqtrade/leverage/liquidation_price.py @@ -19,6 +19,7 @@ def liquidation_price( entry_price_1_both: Optional[float], maintenance_margin_rate_both: Optional[float] ) -> Optional[float]: + if trading_mode == TradingMode.SPOT: return None @@ -29,16 +30,34 @@ 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 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) + 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": diff --git a/tests/leverage/test_liquidation_price.py b/tests/leverage/test_liquidation_price.py index 687dd57f4..ade1d83ea 100644 --- a/tests/leverage/test_liquidation_price.py +++ b/tests/leverage/test_liquidation_price.py @@ -46,7 +46,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,17 +87,14 @@ def test_liquidation_price_exception_thrown( @pytest.mark.parametrize( - 'exchange_name,open_rate,is_short,leverage,trading_mode,collateral,result', [ + ('exchange_name,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,liq_price'), [ # 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), + ("binance", 0.0, False, 1, futures, cross, 1535443.01, + 71200.81144, -56354.57, 135365.00, 3683.979, 1456.84, 0.10, 1153.26) # 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( @@ -100,14 +104,27 @@ def test_liquidation_price( leverage, trading_mode, collateral, - result + wallet_balance, + maintenance_margin_ex_1, + unrealized_pnl_ex_1, + maintenance_amount_both, + position_1_both, + entry_price_1_both, + maintenance_margin_rate_both, + liq_price ): - # assert liquidation_price( - # exchange_name, - # open_rate, - # is_short, - # leverage, - # trading_mode, - # collateral - # ) == result - return # Here to avoid indent error + assert liquidation_price( + exchange_name, + 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 + ) == liq_price From c648b17308ae3f582de19a93a6d58bb914f06429 Mon Sep 17 00:00:00 2001 From: Arunavo Ray Date: Sun, 19 Sep 2021 11:49:08 +0530 Subject: [PATCH 08/22] Flake8 conformant --- freqtrade/leverage/liquidation_price.py | 25 ++++++++++---------- tests/leverage/test_liquidation_price.py | 29 ++++++++++++------------ 2 files changed, 28 insertions(+), 26 deletions(-) diff --git a/freqtrade/leverage/liquidation_price.py b/freqtrade/leverage/liquidation_price.py index 025a64846..9fcf42c38 100644 --- a/freqtrade/leverage/liquidation_price.py +++ b/freqtrade/leverage/liquidation_price.py @@ -29,16 +29,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 \ - or not position_1 or not entry_price_1 or not maintenance_margin_rate: + if not wallet_balance or not maintenance_margin_ex_1 or not unrealized_pnl_ex_1 \ + or not maintenance_amount or not position_1 or not entry_price_1 \ + or not maintenance_margin_rate: raise OperationalException( - f"Parameters wallet_balance, maintenance_margin_ex_1, unrealized_pnl_ex_1, maintenance_amount, " - f"position_1, entry_price_1, maintenance_margin_rate is required by liquidation_price " - f"when exchange is {exchange_name.lower()}") + f"Parameters wallet_balance, maintenance_margin_ex_1, unrealized_pnl_ex_1, " + f"maintenance_amount, position_1, entry_price_1, maintenance_margin_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, position_1, entry_price_1, - maintenance_margin_rate) + return binance(open_rate, is_short, leverage, trading_mode, collateral, wallet_balance, + maintenance_margin_ex_1, unrealized_pnl_ex_1, maintenance_amount, + position_1, entry_price_1, maintenance_margin_rate) elif exchange_name.lower() == "kraken": return kraken(open_rate, is_short, leverage, trading_mode, collateral) elif exchange_name.lower() == "ftx": @@ -87,13 +88,13 @@ 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 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 + If it is an isolated margin mode, then UPNL=0 :param maintenance_amount: Maintenance Amount of position (one-way mode) diff --git a/tests/leverage/test_liquidation_price.py b/tests/leverage/test_liquidation_price.py index 216d0609a..4ea7c8dc2 100644 --- a/tests/leverage/test_liquidation_price.py +++ b/tests/leverage/test_liquidation_price.py @@ -7,22 +7,23 @@ from freqtrade.leverage import liquidation_price @pytest.mark.parametrize( - 'exchange_name, open_rate, is_short, leverage, trading_mode, collateral, wallet_balance, maintenance_margin_ex_1, ' - 'unrealized_pnl_ex_1, maintenance_amount, position_1, entry_price_1, maintenance_margin_rate, ' - 'expected', + 'exchange_name, open_rate, is_short, leverage, trading_mode, collateral, wallet_balance, ' + 'maintenance_margin_ex_1, unrealized_pnl_ex_1, maintenance_amount, position_1, entry_price_1, ' + 'maintenance_margin_rate, expected', [ - ("binance", 0.0, False, 1, TradingMode.FUTURES, Collateral.ISOLATED, 1535443.01, 71200.81144, -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) + ("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, - maintenance_margin_ex_1, unrealized_pnl_ex_1, maintenance_amount, position_1, - entry_price_1, maintenance_margin_rate, expected): +def test_liquidation_price(exchange_name, open_rate, is_short, leverage, trading_mode, collateral, + wallet_balance, maintenance_margin_ex_1, unrealized_pnl_ex_1, + maintenance_amount, position_1, entry_price_1, maintenance_margin_rate, + expected): assert isclose(round(liquidation_price( exchange_name=exchange_name, open_rate=open_rate, From 3cc30e741b21ec5860c80a9bd4fd801e856c41aa Mon Sep 17 00:00:00 2001 From: Arunavo Ray Date: Sun, 19 Sep 2021 11:53:42 +0530 Subject: [PATCH 09/22] isort --- tests/leverage/test_liquidation_price.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/leverage/test_liquidation_price.py b/tests/leverage/test_liquidation_price.py index ee63216f2..92348a75c 100644 --- a/tests/leverage/test_liquidation_price.py +++ b/tests/leverage/test_liquidation_price.py @@ -1,6 +1,7 @@ -import pytest from math import isclose +import pytest + from freqtrade.enums import Collateral, TradingMode from freqtrade.leverage import liquidation_price From da00b21cd50c1341dfd97b67e72257378d5ada74 Mon Sep 17 00:00:00 2001 From: Arunavo Ray Date: Sun, 19 Sep 2021 12:08:50 +0530 Subject: [PATCH 10/22] Shorted parameters --- freqtrade/leverage/liquidation_price.py | 70 ++++++++++++------------ tests/leverage/test_liquidation_price.py | 20 +++---- 2 files changed, 45 insertions(+), 45 deletions(-) diff --git a/freqtrade/leverage/liquidation_price.py b/freqtrade/leverage/liquidation_price.py index c84657bd5..2b94f5d34 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: Optional[float], - position_1: Optional[float], - entry_price_1: Optional[float], - maintenance_margin_rate: 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,17 +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 or not position_1 or not entry_price_1 \ - or not maintenance_margin_rate: + 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, " - f"maintenance_amount, position_1, entry_price_1, maintenance_margin_rate " + 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, - position_1, entry_price_1, maintenance_margin_rate) + 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": @@ -73,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: float, - position_1: float, - entry_price_1: float, - maintenance_margin_rate: float, + mm_ex_1: float, + upnl_ex_1: float, + maintenance_amt: float, + position: float, + entry_price: float, + mm_rate: float, ): r""" Calculates the liquidation price on Binance @@ -91,30 +91,30 @@ def binance( :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, + :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. + :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: Maintenance Amount of position (one-way mode) + :param maintenance_amt: Maintenance Amount of position (one-way mode) - :param position_1: Absolute value of position size (one-way mode) + :param position: Absolute value of position size (one-way mode) - :param entry_price_1: Entry Price of position (one-way mode) + :param entry_price: Entry Price of position (one-way mode) - :param maintenance_margin_rate: Maintenance margin rate 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 - 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 + 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_1 = abs(position_1) - ep1 = entry_price_1 - mmr_b = maintenance_margin_rate + 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 @@ -125,16 +125,16 @@ def binance( # Liquidation Price of USDⓈ-M Futures Contracts Isolated # Isolated margin mode, then TMM=0,UPNL=0 - return (wb + cum_b - side_1 * position_1 * ep1) / ( - position_1 * mmr_b - side_1 * position_1) + 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 - return (wb - tmm_1 + upnl_1 + cum_b - side_1 * position_1 * ep1) / ( - position_1 * mmr_b - side_1 * position_1) + 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 92348a75c..a3f014bb4 100644 --- a/tests/leverage/test_liquidation_price.py +++ b/tests/leverage/test_liquidation_price.py @@ -90,8 +90,8 @@ def test_liquidation_price_exception_thrown( @pytest.mark.parametrize( 'exchange_name, open_rate, is_short, leverage, trading_mode, collateral, wallet_balance, ' - 'maintenance_margin_ex_1, unrealized_pnl_ex_1, maintenance_amount, position_1, entry_price_1, ' - 'maintenance_margin_rate, expected', + '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), @@ -103,8 +103,8 @@ def test_liquidation_price_exception_thrown( -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, maintenance_margin_ex_1, unrealized_pnl_ex_1, - maintenance_amount, position_1, entry_price_1, maintenance_margin_rate, + 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, @@ -114,10 +114,10 @@ def test_liquidation_price(exchange_name, open_rate, is_short, leverage, trading trading_mode=trading_mode, collateral=collateral, wallet_balance=wallet_balance, - maintenance_margin_ex_1=maintenance_margin_ex_1, - unrealized_pnl_ex_1=unrealized_pnl_ex_1, - maintenance_amount=maintenance_amount, - position_1=position_1, - entry_price_1=entry_price_1, - maintenance_margin_rate=maintenance_margin_rate + 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 82acb989a63defc22f19b115cc62120b83d9d135 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sun, 19 Sep 2021 00:51:06 -0600 Subject: [PATCH 11/22] 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 d857c23b7..2ac328a72 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -359,7 +359,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 d0ab95e4e1194846d55473f5bc9b4af73459be67 Mon Sep 17 00:00:00 2001 From: Arunavo Ray Date: Sun, 19 Sep 2021 12:36:01 +0530 Subject: [PATCH 12/22] 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 2b94f5d34..2d75ebd6d 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 " @@ -190,3 +188,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 879bf47b32ff692e35f2509665b1ef9899eb3621 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 19 Sep 2021 19:25:36 +0200 Subject: [PATCH 13/22] Refactor telegram.py to simplify send_msg --- freqtrade/rpc/telegram.py | 51 +++++++++++++++++++++------------------ 1 file changed, 28 insertions(+), 23 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 19c58b63d..898446ea0 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -260,29 +260,7 @@ class Telegram(RPCHandler): return message - def send_msg(self, msg: Dict[str, Any]) -> None: - """ Send a message to telegram channel """ - - default_noti = 'on' - - msg_type = msg['type'] - noti = '' - if msg_type == RPCMessageType.SELL: - sell_noti = self._config['telegram'] \ - .get('notification_settings', {}).get(str(msg_type), {}) - # For backward compatibility sell still can be string - if isinstance(sell_noti, str): - noti = sell_noti - else: - noti = sell_noti.get(str(msg['sell_reason']), default_noti) - else: - noti = self._config['telegram'] \ - .get('notification_settings', {}).get(str(msg_type), default_noti) - - if noti == 'off': - logger.info(f"Notification '{msg_type}' not sent.") - # Notification disabled - return + def compose_message(self, msg: Dict[str, Any], msg_type: RPCMessageType) -> str: if msg_type == RPCMessageType.BUY: message = self._format_buy_msg(msg) @@ -315,6 +293,33 @@ class Telegram(RPCHandler): else: raise NotImplementedError('Unknown message type: {}'.format(msg_type)) + return message + + def send_msg(self, msg: Dict[str, Any]) -> None: + """ Send a message to telegram channel """ + + default_noti = 'on' + + msg_type = msg['type'] + noti = '' + if msg_type == RPCMessageType.SELL: + sell_noti = self._config['telegram'] \ + .get('notification_settings', {}).get(str(msg_type), {}) + # For backward compatibility sell still can be string + if isinstance(sell_noti, str): + noti = sell_noti + else: + noti = sell_noti.get(str(msg['sell_reason']), default_noti) + else: + noti = self._config['telegram'] \ + .get('notification_settings', {}).get(str(msg_type), default_noti) + + if noti == 'off': + logger.info(f"Notification '{msg_type}' not sent.") + # Notification disabled + return + + message = self.compose_message(msg, msg_type) self._send_msg(message, disable_notification=(noti == 'silent')) From 1da091dea3006b67e2400cf1390981354626f4fb Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 19 Sep 2021 19:41:19 +0200 Subject: [PATCH 14/22] ProtectionManager should return the lock just created --- freqtrade/persistence/pairlock_middleware.py | 4 +++- freqtrade/plugins/protectionmanager.py | 23 ++++++++++---------- tests/plugins/test_protections.py | 4 ++-- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/freqtrade/persistence/pairlock_middleware.py b/freqtrade/persistence/pairlock_middleware.py index af904f693..8662fc36d 100644 --- a/freqtrade/persistence/pairlock_middleware.py +++ b/freqtrade/persistence/pairlock_middleware.py @@ -30,7 +30,8 @@ class PairLocks(): PairLocks.locks = [] @staticmethod - def lock_pair(pair: str, until: datetime, reason: str = None, *, now: datetime = None) -> None: + def lock_pair(pair: str, until: datetime, reason: str = None, *, + now: datetime = None) -> PairLock: """ Create PairLock from now to "until". Uses database by default, unless PairLocks.use_db is set to False, @@ -52,6 +53,7 @@ class PairLocks(): PairLock.query.session.commit() else: PairLocks.locks.append(lock) + return lock @staticmethod def get_pair_locks(pair: Optional[str], now: Optional[datetime] = None) -> List[PairLock]: diff --git a/freqtrade/plugins/protectionmanager.py b/freqtrade/plugins/protectionmanager.py index f33e5b4bc..2510d6fee 100644 --- a/freqtrade/plugins/protectionmanager.py +++ b/freqtrade/plugins/protectionmanager.py @@ -6,6 +6,7 @@ from datetime import datetime, timezone from typing import Dict, List, Optional from freqtrade.persistence import PairLocks +from freqtrade.persistence.models import PairLock from freqtrade.plugins.protections import IProtection from freqtrade.resolvers import ProtectionResolver @@ -43,30 +44,28 @@ class ProtectionManager(): """ return [{p.name: p.short_desc()} for p in self._protection_handlers] - def global_stop(self, now: Optional[datetime] = None) -> bool: + def global_stop(self, now: Optional[datetime] = None) -> Optional[PairLock]: if not now: now = datetime.now(timezone.utc) - result = False + result = None for protection_handler in self._protection_handlers: if protection_handler.has_global_stop: - result, until, reason = protection_handler.global_stop(now) + lock, until, reason = protection_handler.global_stop(now) # Early stopping - first positive result blocks further trades - if result and until: + if lock and until: if not PairLocks.is_global_lock(until): - PairLocks.lock_pair('*', until, reason, now=now) - result = True + result = PairLocks.lock_pair('*', until, reason, now=now) return result - def stop_per_pair(self, pair, now: Optional[datetime] = None) -> bool: + def stop_per_pair(self, pair, now: Optional[datetime] = None) -> Optional[PairLock]: if not now: now = datetime.now(timezone.utc) - result = False + result = None for protection_handler in self._protection_handlers: if protection_handler.has_local_stop: - result, until, reason = protection_handler.stop_per_pair(pair, now) - if result and until: + lock, until, reason = protection_handler.stop_per_pair(pair, now) + if lock and until: if not PairLocks.is_pair_locked(pair, until): - PairLocks.lock_pair(pair, until, reason, now=now) - result = True + result = PairLocks.lock_pair(pair, until, reason, now=now) return result diff --git a/tests/plugins/test_protections.py b/tests/plugins/test_protections.py index c0a9ae72a..a3cb29c9d 100644 --- a/tests/plugins/test_protections.py +++ b/tests/plugins/test_protections.py @@ -125,7 +125,7 @@ def test_stoploss_guard(mocker, default_conf, fee, caplog): # Test 5m after lock-period - this should try and relock the pair, but end-time # should be the previous end-time end_time = PairLocks.get_pair_longest_lock('*').lock_end_time + timedelta(minutes=5) - assert freqtrade.protections.global_stop(end_time) + freqtrade.protections.global_stop(end_time) assert not PairLocks.is_global_lock(end_time) @@ -182,7 +182,7 @@ def test_stoploss_guard_perpair(mocker, default_conf, fee, caplog, only_per_pair min_ago_open=180, min_ago_close=30, profit_rate=0.9, )) - assert freqtrade.protections.stop_per_pair(pair) + freqtrade.protections.stop_per_pair(pair) assert freqtrade.protections.global_stop() != only_per_pair assert PairLocks.is_pair_locked(pair) assert PairLocks.is_global_lock() != only_per_pair From c91a9a92f2ccecc5aac46f25a999c0b4a97b20a7 Mon Sep 17 00:00:00 2001 From: Bernhard Millauer Date: Mon, 20 Sep 2021 14:22:24 +0200 Subject: [PATCH 15/22] Add troubleshooting information The time in wsl docker container shifts over time. Added information how to fix this issue. --- docs/docker_quickstart.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/docker_quickstart.md b/docs/docker_quickstart.md index 1fa229225..f4f8c366d 100644 --- a/docs/docker_quickstart.md +++ b/docs/docker_quickstart.md @@ -149,6 +149,20 @@ You'll then also need to modify the `docker-compose.yml` file and uncomment the You can then run `docker-compose build` to build the docker image, and run it using the commands described above. +### Troubleshooting + +### Docker on Windows + +* Error: `"Timestamp for this request is outside of the recvWindow."` + * The market api requests require a synchronized clock but the time in the docker container shifts a bit over time into the past. + To fix this issue temporarily you need to run `wsl --shutdown` and restart docker again (a popup on windows 10 will ask you to do so). + A permanent solution is either to host the docker container on a linux host or restart the wsl from time to time with the scheduler. + ``` + taskkill /IM "Docker Desktop.exe" /F + wsl --shutdown + start "" "C:\Program Files\Docker\Docker\Docker Desktop.exe" + ``` + ## Plotting with docker-compose Commands `freqtrade plot-profit` and `freqtrade plot-dataframe` ([Documentation](plotting.md)) are available by changing the image to `*_plot` in your docker-compose.yml file. From a0fb43c6caca7d0b0f06cf9814d43f69341339b6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 20 Sep 2021 19:12:59 +0200 Subject: [PATCH 16/22] Add pairlock-notification --- freqtrade/enums/rpcmessagetype.py | 1 + freqtrade/freqtradebot.py | 16 ++++++++++++++-- freqtrade/rpc/telegram.py | 5 +++++ tests/rpc/test_rpc_telegram.py | 14 ++++++++++++++ 4 files changed, 34 insertions(+), 2 deletions(-) diff --git a/freqtrade/enums/rpcmessagetype.py b/freqtrade/enums/rpcmessagetype.py index 9c59f6108..889d67cf4 100644 --- a/freqtrade/enums/rpcmessagetype.py +++ b/freqtrade/enums/rpcmessagetype.py @@ -11,6 +11,7 @@ class RPCMessageType(Enum): SELL = 'sell' SELL_FILL = 'sell_fill' SELL_CANCEL = 'sell_cancel' + PROTECTION_TRIGGER = 'protection_trigger' def __repr__(self): return self.value diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 1cb8988ff..451b5764a 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1292,8 +1292,7 @@ class FreqtradeBot(LoggingMixin): if not trade.is_open: if not stoploss_order and not trade.open_order_id: self._notify_exit(trade, '', True) - self.protections.stop_per_pair(trade.pair) - self.protections.global_stop() + self.handle_protections(trade.pair) self.wallets.update() elif not trade.open_order_id: # Buy fill @@ -1301,6 +1300,19 @@ class FreqtradeBot(LoggingMixin): return False + def handle_protections(self, pair: str) -> None: + prot_trig = self.protections.stop_per_pair(pair) + if prot_trig: + msg = {'type': RPCMessageType.PROTECTION_TRIGGER, } + msg.update(prot_trig.to_json()) + self.rpc.send_msg(msg) + + prot_trig_glb = self.protections.global_stop() + if prot_trig_glb: + msg = {'type': RPCMessageType.PROTECTION_TRIGGER, } + msg.update(prot_trig_glb.to_json()) + self.rpc.send_msg(msg) + def apply_fee_conditional(self, trade: Trade, trade_base_currency: str, amount: float, fee_abs: float) -> float: """ diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 898446ea0..934a5182a 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -281,6 +281,11 @@ class Telegram(RPCHandler): "for {close_rate}.".format(**msg)) elif msg_type == RPCMessageType.SELL: message = self._format_sell_msg(msg) + elif msg_type == RPCMessageType.PROTECTION_TRIGGER: + message = ( + "*Protection* triggered due to {reason}. " + "{pair} will be locked until {lock_end_time}." + ).format(**msg) elif msg_type == RPCMessageType.STATUS: message = '*Status:* `{status}`'.format(**msg) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 21f1cd000..0675e4d5f 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1313,6 +1313,20 @@ def test_send_msg_buy_cancel_notification(default_conf, mocker) -> None: 'Reason: cancelled due to timeout.') +def test_send_msg_protection_notification(default_conf, mocker, time_machine) -> None: + + telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) + time_machine.move_to("2021-09-01 05:00:00 +00:00") + lock = PairLocks.lock_pair('ETH/BTC', arrow.utcnow().shift(minutes=6).datetime, 'randreason') + msg = { + 'type': RPCMessageType.PROTECTION_TRIGGER, + } + msg.update(lock.to_json()) + telegram.send_msg(msg) + assert (msg_mock.call_args[0][0] == "*Protection* triggered due to randreason. " + "ETH/BTC will be locked until 2021-09-01 05:10:00.") + + def test_send_msg_buy_fill_notification(default_conf, mocker) -> None: default_conf['telegram']['notification_settings']['buy_fill'] = 'on' From dd0db7ee5db04fb98b542e9541d46768b9bb2c05 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 20 Sep 2021 19:23:40 +0200 Subject: [PATCH 17/22] Split protection-notification into global and per-pair --- config_examples/config_full.example.json | 4 +++- docs/telegram-usage.md | 4 +++- freqtrade/enums/rpcmessagetype.py | 1 + freqtrade/freqtradebot.py | 2 +- freqtrade/rpc/telegram.py | 6 +++++- tests/rpc/test_rpc_telegram.py | 13 +++++++++++++ 6 files changed, 26 insertions(+), 4 deletions(-) diff --git a/config_examples/config_full.example.json b/config_examples/config_full.example.json index d0f3f0df6..c415d70b0 100644 --- a/config_examples/config_full.example.json +++ b/config_examples/config_full.example.json @@ -149,7 +149,9 @@ }, "sell_fill": "on", "buy_cancel": "on", - "sell_cancel": "on" + "sell_cancel": "on", + "protection_trigger": "off", + "protection_trigger_global": "on" }, "reload": true, "balance_dust_level": 0.01 diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index b020b00db..1e6fa9dae 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -93,7 +93,9 @@ Example configuration showing the different settings: "buy_cancel": "silent", "sell_cancel": "on", "buy_fill": "off", - "sell_fill": "off" + "sell_fill": "off", + "protection_trigger": "off", + "protection_trigger_global": "on" }, "reload": true, "balance_dust_level": 0.01 diff --git a/freqtrade/enums/rpcmessagetype.py b/freqtrade/enums/rpcmessagetype.py index 889d67cf4..4e3f693e5 100644 --- a/freqtrade/enums/rpcmessagetype.py +++ b/freqtrade/enums/rpcmessagetype.py @@ -12,6 +12,7 @@ class RPCMessageType(Enum): SELL_FILL = 'sell_fill' SELL_CANCEL = 'sell_cancel' PROTECTION_TRIGGER = 'protection_trigger' + PROTECTION_TRIGGER_GLOBAL = 'protection_trigger_global' def __repr__(self): return self.value diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 451b5764a..37be3173a 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1309,7 +1309,7 @@ class FreqtradeBot(LoggingMixin): prot_trig_glb = self.protections.global_stop() if prot_trig_glb: - msg = {'type': RPCMessageType.PROTECTION_TRIGGER, } + msg = {'type': RPCMessageType.PROTECTION_TRIGGER_GLOBAL, } msg.update(prot_trig_glb.to_json()) self.rpc.send_msg(msg) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 934a5182a..0687f95a2 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -286,7 +286,11 @@ class Telegram(RPCHandler): "*Protection* triggered due to {reason}. " "{pair} will be locked until {lock_end_time}." ).format(**msg) - + elif msg_type == RPCMessageType.PROTECTION_TRIGGER_GLOBAL: + message = ( + "*Protection* triggered due to {reason}. " + "All pairs will be locked until {lock_end_time}." + ).format(**msg) elif msg_type == RPCMessageType.STATUS: message = '*Status:* `{status}`'.format(**msg) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 0675e4d5f..d37a74187 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1326,6 +1326,19 @@ def test_send_msg_protection_notification(default_conf, mocker, time_machine) -> assert (msg_mock.call_args[0][0] == "*Protection* triggered due to randreason. " "ETH/BTC will be locked until 2021-09-01 05:10:00.") + msg_mock.reset_mock() + # Test global protection + + msg = { + 'type': RPCMessageType.PROTECTION_TRIGGER_GLOBAL, + } + lock = PairLocks.lock_pair('*', arrow.utcnow().shift(minutes=100).datetime, 'randreason') + msg.update(lock.to_json()) + telegram.send_msg(msg) + assert (msg_mock.call_args[0][0] == "*Protection* triggered due to randreason. " + "All pairs will be locked until 2021-09-01 06:45:00.") + + def test_send_msg_buy_fill_notification(default_conf, mocker) -> None: From fd23ab3d647db7444d12129755ddf001e014dbcd Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 20 Sep 2021 19:49:18 +0200 Subject: [PATCH 18/22] improve formatting, add tests --- freqtrade/rpc/telegram.py | 4 ++-- tests/rpc/test_rpc_telegram.py | 5 ++--- tests/test_freqtradebot.py | 23 +++++++++++++++++++++++ 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 0687f95a2..059ba9c41 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -284,12 +284,12 @@ class Telegram(RPCHandler): elif msg_type == RPCMessageType.PROTECTION_TRIGGER: message = ( "*Protection* triggered due to {reason}. " - "{pair} will be locked until {lock_end_time}." + "`{pair}` will be locked until `{lock_end_time}`." ).format(**msg) elif msg_type == RPCMessageType.PROTECTION_TRIGGER_GLOBAL: message = ( "*Protection* triggered due to {reason}. " - "All pairs will be locked until {lock_end_time}." + "*All pairs* will be locked until `{lock_end_time}`." ).format(**msg) elif msg_type == RPCMessageType.STATUS: message = '*Status:* `{status}`'.format(**msg) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index d37a74187..8c285a76e 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1324,7 +1324,7 @@ def test_send_msg_protection_notification(default_conf, mocker, time_machine) -> msg.update(lock.to_json()) telegram.send_msg(msg) assert (msg_mock.call_args[0][0] == "*Protection* triggered due to randreason. " - "ETH/BTC will be locked until 2021-09-01 05:10:00.") + "`ETH/BTC` will be locked until `2021-09-01 05:10:00`.") msg_mock.reset_mock() # Test global protection @@ -1336,8 +1336,7 @@ def test_send_msg_protection_notification(default_conf, mocker, time_machine) -> msg.update(lock.to_json()) telegram.send_msg(msg) assert (msg_mock.call_args[0][0] == "*Protection* triggered due to randreason. " - "All pairs will be locked until 2021-09-01 06:45:00.") - + "*All pairs* will be locked until `2021-09-01 06:45:00`.") def test_send_msg_buy_fill_notification(default_conf, mocker) -> None: diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index eb3c77cc7..e1df6dad9 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -526,6 +526,29 @@ def test_enter_positions_global_pairlock(default_conf, ticker, limit_buy_order, assert log_has_re(message, caplog) +def test_handle_protections(mocker, default_conf, fee): + default_conf['protections'] = [ + {"method": "CooldownPeriod", "stop_duration": 60}, + { + "method": "StoplossGuard", + "lookback_period_candles": 24, + "trade_limit": 4, + "stop_duration_candles": 4, + "only_per_pair": False + } + ] + + freqtrade = get_patched_freqtradebot(mocker, default_conf) + freqtrade.protections._protection_handlers[1].global_stop = MagicMock( + return_value=(True, arrow.utcnow().shift(hours=1).datetime, "asdf")) + create_mock_trades(fee) + freqtrade.handle_protections('ETC/BTC') + send_msg_mock = freqtrade.rpc.send_msg + assert send_msg_mock.call_count == 2 + assert send_msg_mock.call_args_list[0][0][0]['type'] == RPCMessageType.PROTECTION_TRIGGER + assert send_msg_mock.call_args_list[1][0][0]['type'] == RPCMessageType.PROTECTION_TRIGGER_GLOBAL + + def test_create_trade_no_signal(default_conf, fee, mocker) -> None: default_conf['dry_run'] = True From 3ce05c0d548119fd5618ba2700313a69ec10f3c5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 20 Sep 2021 20:08:48 +0200 Subject: [PATCH 19/22] Add "sane" defaults to protection triggers --- docs/telegram-usage.md | 1 + freqtrade/constants.py | 9 +++++++++ tests/rpc/test_rpc_telegram.py | 2 ++ 3 files changed, 12 insertions(+) diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index 1e6fa9dae..b9d01a236 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -105,6 +105,7 @@ Example configuration showing the different settings: `buy` notifications are sent when the order is placed, while `buy_fill` notifications are sent when the order is filled on the exchange. `sell` notifications are sent when the order is placed, while `sell_fill` notifications are sent when the order is filled on the exchange. `*_fill` notifications are off by default and must be explicitly enabled. +`protection_trigger` notifications are sent when a protection triggers and `protection_trigger_global` notifications trigger when global protections are triggered. `balance_dust_level` will define what the `/balance` command takes as "dust" - Currencies with a balance below this will be shown. diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 9ca43d459..4997108bc 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -284,6 +284,15 @@ CONF_SCHEMA = { 'enum': TELEGRAM_SETTING_OPTIONS, 'default': 'off' }, + 'protection_trigger': { + 'type': 'string', + 'enum': TELEGRAM_SETTING_OPTIONS, + 'default': 'off' + }, + 'protection_trigger_global': { + 'type': 'string', + 'enum': TELEGRAM_SETTING_OPTIONS, + }, } }, 'reload': {'type': 'boolean'}, diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 8c285a76e..7dde7b803 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1315,6 +1315,8 @@ def test_send_msg_buy_cancel_notification(default_conf, mocker) -> None: def test_send_msg_protection_notification(default_conf, mocker, time_machine) -> None: + default_conf['telegram']['notification_settings']['protection_trigger'] = 'on' + telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) time_machine.move_to("2021-09-01 05:00:00 +00:00") lock = PairLocks.lock_pair('ETH/BTC', arrow.utcnow().shift(minutes=6).datetime, 'randreason') From abddb0db66b431abf5902a8fa74dbf5f2cf16e56 Mon Sep 17 00:00:00 2001 From: Bernhard Millauer Date: Tue, 21 Sep 2021 10:13:19 +0200 Subject: [PATCH 20/22] Fix header indention Co-authored-by: Matthias --- docs/docker_quickstart.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docker_quickstart.md b/docs/docker_quickstart.md index f4f8c366d..33b1c7ea1 100644 --- a/docs/docker_quickstart.md +++ b/docs/docker_quickstart.md @@ -151,7 +151,7 @@ You can then run `docker-compose build` to build the docker image, and run it us ### Troubleshooting -### Docker on Windows +#### Docker on Windows * Error: `"Timestamp for this request is outside of the recvWindow."` * The market api requests require a synchronized clock but the time in the docker container shifts a bit over time into the past. From 6fc770d97dd8d4380be3fb94e8f3dcedfa981cd7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 21 Sep 2021 15:12:35 +0200 Subject: [PATCH 21/22] Add warning about running with docker on windows --- docs/docker_quickstart.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/docker_quickstart.md b/docs/docker_quickstart.md index 33b1c7ea1..2f350d207 100644 --- a/docs/docker_quickstart.md +++ b/docs/docker_quickstart.md @@ -163,6 +163,10 @@ You can then run `docker-compose build` to build the docker image, and run it us start "" "C:\Program Files\Docker\Docker\Docker Desktop.exe" ``` +!!! Warning + Due to the above, we do not recommend the usage of docker on windows for production setups, but only for experimentation, datadownload and backtesting. + Best use a linux-VPS for running freqtrade reliably. + ## Plotting with docker-compose Commands `freqtrade plot-profit` and `freqtrade plot-dataframe` ([Documentation](plotting.md)) are available by changing the image to `*_plot` in your docker-compose.yml file. From 375949e41a6fe60dd5e1ae279d58cd142a54b042 Mon Sep 17 00:00:00 2001 From: Arunavo Ray Date: Thu, 23 Sep 2021 11:54:05 +0530 Subject: [PATCH 22/22] 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 2d75ebd6d..e322ddb34 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":