From 1ad177fca7c8bf4cae34063d9bb95409003650f4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 24 Aug 2024 16:31:13 +0200 Subject: [PATCH 01/25] feat: add liquidation_price update support for cross mode --- freqtrade/freqtradebot.py | 51 ++++++++++++++++++++++++++++++--------- 1 file changed, 40 insertions(+), 11 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index fe270f670..d65ee8d0f 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -2157,6 +2157,45 @@ class FreqtradeBot(LoggingMixin): # Common update trade state methods # + def update_liquidation_prices(self, trade: Optional[Trade] = None): + """ + Update trade liquidation price in isolated margin mode. + Updates liquidation price for all trades in cross margin mode. + TODO: this is missing a dedicated test! + """ + total_wallet_stake = 0.0 + if self.config["dry_run"] and self.exchange.margin_mode == MarginMode.CROSS: + # Parameters only needed for cross margin + total_wallet_stake = self.wallets.get_total(self.config["stake_currency"]) + + if self.exchange.margin_mode == MarginMode.CROSS: + logger.info("Updating liquidation price for all open trades.") + for t in Trade.get_open_trades(): + # TODO: This should be done in a batch update + t.set_liquidation_price( + self.exchange.get_liquidation_price( + pair=t.pair, + open_rate=t.open_rate, + is_short=t.is_short, + amount=t.amount, + stake_amount=t.stake_amount, + leverage=trade.leverage, + wallet_balance=total_wallet_stake, + ) + ) + elif trade: + trade.set_liquidation_price( + self.exchange.get_liquidation_price( + pair=trade.pair, + open_rate=trade.open_rate, + is_short=trade.is_short, + amount=trade.amount, + stake_amount=trade.stake_amount, + leverage=trade.leverage, + wallet_balance=trade.stake_amount, + ) + ) + def update_trade_state( self, trade: Trade, @@ -2234,17 +2273,7 @@ class FreqtradeBot(LoggingMixin): # TODO: Margin will need to use interest_rate as well. # interest_rate = self.exchange.get_interest_rate() try: - trade.set_liquidation_price( - self.exchange.get_liquidation_price( - pair=trade.pair, - open_rate=trade.open_rate, - is_short=trade.is_short, - amount=trade.amount, - stake_amount=trade.stake_amount, - leverage=trade.leverage, - wallet_balance=trade.stake_amount, - ) - ) + self.update_liquidation_prices(trade) except DependencyException: logger.warning("Unable to calculate liquidation price") if self.strategy.use_custom_stoploss: From b69f598e51986752395ba60ea8e18cd4a883d9f6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 24 Aug 2024 16:34:03 +0200 Subject: [PATCH 02/25] refactor: move more code into cross conditional --- freqtrade/freqtradebot.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index d65ee8d0f..7e826d19e 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -2163,12 +2163,11 @@ class FreqtradeBot(LoggingMixin): Updates liquidation price for all trades in cross margin mode. TODO: this is missing a dedicated test! """ - total_wallet_stake = 0.0 - if self.config["dry_run"] and self.exchange.margin_mode == MarginMode.CROSS: - # Parameters only needed for cross margin - total_wallet_stake = self.wallets.get_total(self.config["stake_currency"]) - if self.exchange.margin_mode == MarginMode.CROSS: + total_wallet_stake = 0.0 + if self.config["dry_run"]: + # Parameters only needed for cross margin + total_wallet_stake = self.wallets.get_total(self.config["stake_currency"]) logger.info("Updating liquidation price for all open trades.") for t in Trade.get_open_trades(): # TODO: This should be done in a batch update From 3de740b35ffb34ba2a824c53867bf81c1f4fe9ae Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 24 Aug 2024 16:46:52 +0200 Subject: [PATCH 03/25] feat: create shared method for liquidation price update --- freqtrade/freqtradebot.py | 48 ++++----------------- freqtrade/leverage/liquidation_price.py | 56 +++++++++++++++++++++++++ freqtrade/optimize/backtesting.py | 17 ++++---- 3 files changed, 71 insertions(+), 50 deletions(-) create mode 100644 freqtrade/leverage/liquidation_price.py diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 7e826d19e..3c1122c9a 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -22,7 +22,6 @@ from freqtrade.edge import Edge from freqtrade.enums import ( ExitCheckTuple, ExitType, - MarginMode, RPCMessageType, SignalDirection, State, @@ -43,6 +42,7 @@ from freqtrade.exchange import ( timeframe_to_next_date, timeframe_to_seconds, ) +from freqtrade.leverage.liquidation_price import update_liquidation_prices from freqtrade.misc import safe_value_fallback, safe_value_fallback2 from freqtrade.mixins import LoggingMixin from freqtrade.persistence import Order, PairLocks, Trade, init_db @@ -2157,44 +2157,6 @@ class FreqtradeBot(LoggingMixin): # Common update trade state methods # - def update_liquidation_prices(self, trade: Optional[Trade] = None): - """ - Update trade liquidation price in isolated margin mode. - Updates liquidation price for all trades in cross margin mode. - TODO: this is missing a dedicated test! - """ - if self.exchange.margin_mode == MarginMode.CROSS: - total_wallet_stake = 0.0 - if self.config["dry_run"]: - # Parameters only needed for cross margin - total_wallet_stake = self.wallets.get_total(self.config["stake_currency"]) - logger.info("Updating liquidation price for all open trades.") - for t in Trade.get_open_trades(): - # TODO: This should be done in a batch update - t.set_liquidation_price( - self.exchange.get_liquidation_price( - pair=t.pair, - open_rate=t.open_rate, - is_short=t.is_short, - amount=t.amount, - stake_amount=t.stake_amount, - leverage=trade.leverage, - wallet_balance=total_wallet_stake, - ) - ) - elif trade: - trade.set_liquidation_price( - self.exchange.get_liquidation_price( - pair=trade.pair, - open_rate=trade.open_rate, - is_short=trade.is_short, - amount=trade.amount, - stake_amount=trade.stake_amount, - leverage=trade.leverage, - wallet_balance=trade.stake_amount, - ) - ) - def update_trade_state( self, trade: Trade, @@ -2272,7 +2234,13 @@ class FreqtradeBot(LoggingMixin): # TODO: Margin will need to use interest_rate as well. # interest_rate = self.exchange.get_interest_rate() try: - self.update_liquidation_prices(trade) + update_liquidation_prices( + trade, + exchange=self.exchange, + wallets=self.wallets, + stake_currency=self.config["stake_currency"], + dry_run=self.config["dry_run"], + ) except DependencyException: logger.warning("Unable to calculate liquidation price") if self.strategy.use_custom_stoploss: diff --git a/freqtrade/leverage/liquidation_price.py b/freqtrade/leverage/liquidation_price.py new file mode 100644 index 000000000..b1647efd2 --- /dev/null +++ b/freqtrade/leverage/liquidation_price.py @@ -0,0 +1,56 @@ +import logging + +from freqtrade.enums import MarginMode +from freqtrade.exchange import Exchange +from freqtrade.persistence import LocalTrade, Trade +from freqtrade.wallets import Wallets + + +logger = logging.getLogger(__name__) + + +def update_liquidation_prices( + trade: LocalTrade, + *, + exchange: Exchange, + wallets: Wallets, + stake_currency: str, + dry_run: bool = False, +): + """ + Update trade liquidation price in isolated margin mode. + Updates liquidation price for all trades in cross margin mode. + TODO: this is missing a dedicated test! + """ + if exchange.margin_mode == MarginMode.CROSS: + total_wallet_stake = 0.0 + if dry_run: + # Parameters only needed for cross margin + total_wallet_stake = wallets.get_total(stake_currency) + + logger.info("Updating liquidation price for all open trades.") + for t in Trade.get_open_trades(): + # TODO: This should be done in a batch update + t.set_liquidation_price( + exchange.get_liquidation_price( + pair=t.pair, + open_rate=t.open_rate, + is_short=t.is_short, + amount=t.amount, + stake_amount=t.stake_amount, + leverage=trade.leverage, + wallet_balance=total_wallet_stake, + ) + ) + elif trade: + trade.set_liquidation_price( + exchange.get_liquidation_price( + pair=trade.pair, + open_rate=trade.open_rate, + is_short=trade.is_short, + amount=trade.amount, + stake_amount=trade.stake_amount, + leverage=trade.leverage, + wallet_balance=trade.stake_amount, + ) + ) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 75c0ac075..0d7967762 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -37,6 +37,7 @@ from freqtrade.exchange import ( ) from freqtrade.exchange.exchange import Exchange from freqtrade.ft_types import BacktestResultType, get_BacktestResultType_default +from freqtrade.leverage.liquidation_price import update_liquidation_prices from freqtrade.mixins import LoggingMixin from freqtrade.optimize.backtest_caching import get_strategy_run_id from freqtrade.optimize.bt_progress import BTProgress @@ -700,16 +701,12 @@ class Backtesting: if not (order.ft_order_side == trade.exit_side and order.safe_amount == trade.amount): # trade is still open - trade.set_liquidation_price( - self.exchange.get_liquidation_price( - pair=trade.pair, - open_rate=trade.open_rate, - is_short=trade.is_short, - amount=trade.amount, - stake_amount=trade.stake_amount, - leverage=trade.leverage, - wallet_balance=trade.stake_amount, - ) + update_liquidation_prices( + trade, + exchange=self.exchange, + wallets=self.wallets, + stake_currency=self.config["stake_currency"], + dry_run=self.config["dry_run"], ) self._call_adjust_stop(current_date, trade, order.ft_price) # pass From 05605670585c94f504f97b020b94a3c38bbb156e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 25 Aug 2024 18:14:26 +0200 Subject: [PATCH 04/25] test: add test for liquidation_price update function --- freqtrade/leverage/liquidation_price.py | 3 +- .../leverage/test_update_liquidation_price.py | 57 +++++++++++++++++++ 2 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 tests/leverage/test_update_liquidation_price.py diff --git a/freqtrade/leverage/liquidation_price.py b/freqtrade/leverage/liquidation_price.py index b1647efd2..17526cb58 100644 --- a/freqtrade/leverage/liquidation_price.py +++ b/freqtrade/leverage/liquidation_price.py @@ -20,7 +20,6 @@ def update_liquidation_prices( """ Update trade liquidation price in isolated margin mode. Updates liquidation price for all trades in cross margin mode. - TODO: this is missing a dedicated test! """ if exchange.margin_mode == MarginMode.CROSS: total_wallet_stake = 0.0 @@ -42,7 +41,7 @@ def update_liquidation_prices( wallet_balance=total_wallet_stake, ) ) - elif trade: + else: trade.set_liquidation_price( exchange.get_liquidation_price( pair=trade.pair, diff --git a/tests/leverage/test_update_liquidation_price.py b/tests/leverage/test_update_liquidation_price.py new file mode 100644 index 000000000..1b25babd0 --- /dev/null +++ b/tests/leverage/test_update_liquidation_price.py @@ -0,0 +1,57 @@ +from unittest.mock import MagicMock + +import pytest + +from freqtrade.enums.marginmode import MarginMode +from freqtrade.leverage.liquidation_price import update_liquidation_prices + + +@pytest.mark.parametrize("dry_run", [False, True]) +@pytest.mark.parametrize("margin_mode", [MarginMode.CROSS, MarginMode.ISOLATED]) +def test_update_liquidation_prices(mocker, margin_mode, dry_run): + # Heavily mocked test - Only testing the logic of the function + # update liquidation price for trade in isolated mode + # update liquidation price for all trades in cross mode + exchange = MagicMock() + exchange.margin_mode = margin_mode + wallets = MagicMock() + trade_mock = MagicMock() + + mocker.patch("freqtrade.persistence.Trade.get_open_trades", return_value=[trade_mock]) + + update_liquidation_prices( + trade=trade_mock, + exchange=exchange, + wallets=wallets, + stake_currency="USDT", + dry_run=dry_run, + ) + + assert trade_mock.set_liquidation_price.call_count == 1 + + assert wallets.get_total.call_count == ( + 0 if margin_mode == MarginMode.ISOLATED or not dry_run else 1 + ) + + # Test with multiple trades + trade_mock.reset_mock() + trade_mock_2 = MagicMock() + + mocker.patch( + "freqtrade.persistence.Trade.get_open_trades", return_value=[trade_mock, trade_mock_2] + ) + + update_liquidation_prices( + trade=trade_mock, + exchange=exchange, + wallets=wallets, + stake_currency="USDT", + dry_run=dry_run, + ) + # Trade2 is only updated in cross mode + assert trade_mock_2.set_liquidation_price.call_count == ( + 1 if margin_mode == MarginMode.CROSS else 0 + ) + assert trade_mock.set_liquidation_price.call_count == 1 + + assert wallets.call_count == 0 if not dry_run else 1 From ec79b0b17b8f9bd82b74072cc1b1d215c0ae7de7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 29 Aug 2024 19:55:21 +0200 Subject: [PATCH 05/25] feat: update dry-run calculation params to be more generic --- freqtrade/exchange/binance.py | 26 +++++++++++++++++++++----- freqtrade/exchange/bybit.py | 4 ++-- freqtrade/exchange/exchange.py | 14 ++++---------- 3 files changed, 27 insertions(+), 17 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index d7fb0a353..637bfafef 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -153,8 +153,7 @@ class Binance(Exchange): stake_amount: float, leverage: float, wallet_balance: float, # Or margin balance - mm_ex_1: float = 0.0, # (Binance) Cross only - upnl_ex_1: float = 0.0, # (Binance) Cross only + other_trades: list, ) -> Optional[float]: """ Important: Must be fetching data from cached values as this is used by backtesting! @@ -172,6 +171,7 @@ class Binance(Exchange): :param wallet_balance: Amount of margin_mode in the wallet being used to trade Cross-Margin Mode: crossWalletBalance Isolated-Margin Mode: isolatedWalletBalance + :param other_trades: List of other open trades in the same wallet # * Only required for Cross :param mm_ex_1: (TMM) @@ -180,15 +180,31 @@ class Binance(Exchange): :param upnl_ex_1: (UPNL) Cross-Margin Mode: Unrealized PNL of all other contracts, excluding Contract 1. Isolated-Margin Mode: 0 + :param other """ - - side_1 = -1 if is_short else 1 - cross_vars = upnl_ex_1 - mm_ex_1 if self.margin_mode == MarginMode.CROSS else 0.0 + cross_vars: float = 0.0 # mm_ratio: Binance's formula specifies maintenance margin rate which is mm_ratio * 100% # maintenance_amt: (CUM) Maintenance Amount of position mm_ratio, maintenance_amt = self.get_maintenance_ratio_and_amt(pair, stake_amount) + if self.margin_mode == MarginMode.CROSS: + mm_ex_1: float = 0.0 + upnl_ex_1: float = 0.0 + for trade in other_trades: + mm_ratio1, maint_amnt1 = self.get_maintenance_ratio_and_amt( + trade["pair"], trade["stake_amount"] + ) + maint_margin = trade["amount"] * trade["mark_price"] * mm_ratio1 - maint_amnt1 + mm_ex_1 += maint_margin + + upnl_ex_1 += ( + trade["amount"] * trade["mark_price"] - trade["amount"] * trade["open_rate"] + ) + cross_vars = upnl_ex_1 - mm_ex_1 + + side_1 = -1 if is_short else 1 + if maintenance_amt is None: raise OperationalException( "Parameter maintenance_amt is required by Binance.liquidation_price" diff --git a/freqtrade/exchange/bybit.py b/freqtrade/exchange/bybit.py index 719c64dc3..c14273e53 100644 --- a/freqtrade/exchange/bybit.py +++ b/freqtrade/exchange/bybit.py @@ -147,8 +147,7 @@ class Bybit(Exchange): stake_amount: float, leverage: float, wallet_balance: float, # Or margin balance - mm_ex_1: float = 0.0, # (Binance) Cross only - upnl_ex_1: float = 0.0, # (Binance) Cross only + other_trades: list, ) -> Optional[float]: """ Important: Must be fetching data from cached values as this is used by backtesting! @@ -178,6 +177,7 @@ class Bybit(Exchange): :param wallet_balance: Amount of margin_mode in the wallet being used to trade Cross-Margin Mode: crossWalletBalance Isolated-Margin Mode: isolatedWalletBalance + :param other_trades: List of other open trades in the same wallet """ market = self.markets[pair] diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 9d84f59e4..83aff2def 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -3532,8 +3532,7 @@ class Exchange: stake_amount: float, leverage: float, wallet_balance: float, - mm_ex_1: float = 0.0, # (Binance) Cross only - upnl_ex_1: float = 0.0, # (Binance) Cross only + other_trades: list, ) -> Optional[float]: """ Set's the margin mode on the exchange to cross or isolated for a specific pair @@ -3555,8 +3554,7 @@ class Exchange: leverage=leverage, stake_amount=stake_amount, wallet_balance=wallet_balance, - mm_ex_1=mm_ex_1, - upnl_ex_1=upnl_ex_1, + other_trades=other_trades, ) else: positions = self.fetch_positions(pair) @@ -3582,8 +3580,7 @@ class Exchange: stake_amount: float, leverage: float, wallet_balance: float, # Or margin balance - mm_ex_1: float = 0.0, # (Binance) Cross only - upnl_ex_1: float = 0.0, # (Binance) Cross only + other_trades: list, ) -> Optional[float]: """ Important: Must be fetching data from cached values as this is used by backtesting! @@ -3608,10 +3605,7 @@ class Exchange: :param wallet_balance: Amount of margin_mode in the wallet being used to trade Cross-Margin Mode: crossWalletBalance Isolated-Margin Mode: isolatedWalletBalance - - # * Not required by Gate or OKX - :param mm_ex_1: - :param upnl_ex_1: + :param other_trades: List of other open trades in the same wallet """ market = self.markets[pair] From 82bc3270e75b32e6e8fbe8e3bf9e27fc5e6f4532 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 29 Aug 2024 19:56:24 +0200 Subject: [PATCH 06/25] test: Update binance test for new approach --- tests/exchange/test_binance.py | 74 +++++++++++++++++++++++++--------- 1 file changed, 54 insertions(+), 20 deletions(-) diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index 7b0831520..96f3e6a5b 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -172,7 +172,7 @@ def test_stoploss_adjust_binance(mocker, default_conf, sl1, sl2, sl3, side): @pytest.mark.parametrize( "is_short, trading_mode, margin_mode, wallet_balance, " - "mm_ex_1, upnl_ex_1, maintenance_amt, amount, open_rate, " + "maintenance_amt, amount, open_rate, mark_price, other_contracts," "mm_ratio, expected", [ ( @@ -180,11 +180,11 @@ def test_stoploss_adjust_binance(mocker, default_conf, sl1, sl2, sl3, side): "futures", "isolated", 1535443.01, - 0.0, - 0.0, 135365.00, 3683.979, 1456.84, + 1456.84, # mark price + [], 0.10, 1114.78, ), @@ -193,11 +193,11 @@ def test_stoploss_adjust_binance(mocker, default_conf, sl1, sl2, sl3, side): "futures", "isolated", 1535443.01, - 0.0, - 0.0, 16300.000, 109.488, 32481.980, + 32481.980, + [], 0.025, 18778.73, ), @@ -206,11 +206,24 @@ def test_stoploss_adjust_binance(mocker, default_conf, sl1, sl2, sl3, side): "futures", "cross", 1535443.01, - 71200.81144, - -56354.57, + # 71200.81144, # tmm1 + # -56354.57, # upnl1 135365.00, - 3683.979, - 1456.84, + 3683.979, # amount + 1456.84, # open_rate + 1335.18, # mark_price + [ + { + # From calc example + "pair": "BTC/USDT:USDT", + "open_rate": 32481.98, + "amount": 109.488, + "stake_amount": 3556387.02624, # open_rate * amount + "mark_price": 31967.27, + "mm_ratio": 0.025, + "maintenance_amt": 16300.0, + } + ], 0.10, 1153.26, ), @@ -219,11 +232,24 @@ def test_stoploss_adjust_binance(mocker, default_conf, sl1, sl2, sl3, side): "futures", "cross", 1535443.01, - 356512.508, - -448192.89, - 16300.000, - 109.488, - 32481.980, + # 356512.508, # tmm1 + # -448192.89, # upnl1 + 16300.0, + 109.488, # amount + 32481.980, # open_rate + 31967.27, # mark_price + [ + { + # From calc example + "pair": "ETH/USDT:USDT", + "open_rate": 1456.84, + "amount": 3683.979, + "stake_amount": 5366967.96, + "mark_price": 1335.18, + "mm_ratio": 0.10, + "maintenance_amt": 135365.00, + } + ], 0.025, 26316.89, ), @@ -232,15 +258,15 @@ def test_stoploss_adjust_binance(mocker, default_conf, sl1, sl2, sl3, side): def test_liquidation_price_binance( mocker, default_conf, - open_rate, is_short, trading_mode, margin_mode, wallet_balance, - mm_ex_1, - upnl_ex_1, maintenance_amt, amount, + open_rate, + mark_price, + other_contracts, mm_ratio, expected, ): @@ -248,7 +274,14 @@ def test_liquidation_price_binance( default_conf["margin_mode"] = margin_mode default_conf["liquidation_buffer"] = 0.0 exchange = get_patched_exchange(mocker, default_conf, exchange="binance") - exchange.get_maintenance_ratio_and_amt = MagicMock(return_value=(mm_ratio, maintenance_amt)) + + def get_maint_ratio(pair, stake_amount): + if pair != "DOGE/USDT": + oc = [c for c in other_contracts if c["pair"] == pair][0] + return oc["mm_ratio"], oc["maintenance_amt"] + return mm_ratio, maintenance_amt + + exchange.get_maintenance_ratio_and_amt = get_maint_ratio assert ( pytest.approx( round( @@ -257,11 +290,12 @@ def test_liquidation_price_binance( open_rate=open_rate, is_short=is_short, wallet_balance=wallet_balance, - mm_ex_1=mm_ex_1, - upnl_ex_1=upnl_ex_1, amount=amount, stake_amount=open_rate * amount, leverage=5, + other_trades=other_contracts, + # mm_ex_1=mm_ex_1, + # upnl_ex_1=upnl_ex_1, ), 2, ) From 0c0bb29f8370602122b2b04c4606cd1ea6418fe5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 29 Aug 2024 20:01:44 +0200 Subject: [PATCH 07/25] chore: add other_trades param to liquidation_price calls --- freqtrade/leverage/liquidation_price.py | 2 ++ tests/exchange/test_exchange.py | 8 ++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/freqtrade/leverage/liquidation_price.py b/freqtrade/leverage/liquidation_price.py index 17526cb58..4053ba799 100644 --- a/freqtrade/leverage/liquidation_price.py +++ b/freqtrade/leverage/liquidation_price.py @@ -39,6 +39,7 @@ def update_liquidation_prices( stake_amount=t.stake_amount, leverage=trade.leverage, wallet_balance=total_wallet_stake, + other_trades=[], # TODO: Add other trades ) ) else: @@ -51,5 +52,6 @@ def update_liquidation_prices( stake_amount=trade.stake_amount, leverage=trade.leverage, wallet_balance=trade.stake_amount, + other_trades=[], ) ) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index d71d2062e..56a967e5a 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -5524,8 +5524,7 @@ def test_liquidation_price_is_none( stake_amount=open_rate * 71200.81144, leverage=5, wallet_balance=-56354.57, - mm_ex_1=0.10, - upnl_ex_1=0.0, + other_trades=[], ) is None ) @@ -5971,6 +5970,7 @@ def test_get_liquidation_price1(mocker, default_conf): stake_amount=18.884 * 0.8, leverage=leverage, wallet_balance=18.884 * 0.8, + other_trades=[], ) assert liq_price == 17.47 @@ -5984,6 +5984,7 @@ def test_get_liquidation_price1(mocker, default_conf): stake_amount=18.884 * 0.8, leverage=leverage, wallet_balance=18.884 * 0.8, + other_trades=[], ) assert liq_price == 17.540699999999998 @@ -5997,6 +5998,7 @@ def test_get_liquidation_price1(mocker, default_conf): stake_amount=18.884 * 0.8, leverage=leverage, wallet_balance=18.884 * 0.8, + other_trades=[], ) assert liq_price is None default_conf["trading_mode"] = "margin" @@ -6011,6 +6013,7 @@ def test_get_liquidation_price1(mocker, default_conf): stake_amount=18.884 * 0.8, leverage=leverage, wallet_balance=18.884 * 0.8, + other_trades=[], ) @@ -6141,6 +6144,7 @@ def test_get_liquidation_price( wallet_balance=amount * open_rate / leverage, leverage=leverage, is_short=is_short, + other_trades=[], ) if expected_liq is None: assert liq is None From c316d274443ad3840f057c235ca509034ac01494 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 30 Aug 2024 07:07:22 +0200 Subject: [PATCH 08/25] refactor: move exception handler into helper function --- freqtrade/freqtradebot.py | 17 +++---- freqtrade/leverage/liquidation_price.py | 62 +++++++++++++------------ 2 files changed, 40 insertions(+), 39 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 3c1122c9a..5097722fe 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -2233,16 +2233,13 @@ class FreqtradeBot(LoggingMixin): # Must also run for partial exits # TODO: Margin will need to use interest_rate as well. # interest_rate = self.exchange.get_interest_rate() - try: - update_liquidation_prices( - trade, - exchange=self.exchange, - wallets=self.wallets, - stake_currency=self.config["stake_currency"], - dry_run=self.config["dry_run"], - ) - except DependencyException: - logger.warning("Unable to calculate liquidation price") + update_liquidation_prices( + trade, + exchange=self.exchange, + wallets=self.wallets, + stake_currency=self.config["stake_currency"], + dry_run=self.config["dry_run"], + ) if self.strategy.use_custom_stoploss: current_rate = self.exchange.get_rate( trade.pair, side="exit", is_short=trade.is_short, refresh=True diff --git a/freqtrade/leverage/liquidation_price.py b/freqtrade/leverage/liquidation_price.py index 4053ba799..6b51397b5 100644 --- a/freqtrade/leverage/liquidation_price.py +++ b/freqtrade/leverage/liquidation_price.py @@ -1,6 +1,7 @@ import logging from freqtrade.enums import MarginMode +from freqtrade.exceptions import DependencyException from freqtrade.exchange import Exchange from freqtrade.persistence import LocalTrade, Trade from freqtrade.wallets import Wallets @@ -21,37 +22,40 @@ def update_liquidation_prices( Update trade liquidation price in isolated margin mode. Updates liquidation price for all trades in cross margin mode. """ - if exchange.margin_mode == MarginMode.CROSS: - total_wallet_stake = 0.0 - if dry_run: - # Parameters only needed for cross margin - total_wallet_stake = wallets.get_total(stake_currency) + try: + if exchange.margin_mode == MarginMode.CROSS: + total_wallet_stake = 0.0 + if dry_run: + # Parameters only needed for cross margin + total_wallet_stake = wallets.get_total(stake_currency) - logger.info("Updating liquidation price for all open trades.") - for t in Trade.get_open_trades(): - # TODO: This should be done in a batch update - t.set_liquidation_price( + logger.info("Updating liquidation price for all open trades.") + for t in Trade.get_open_trades(): + # TODO: This should be done in a batch update + t.set_liquidation_price( + exchange.get_liquidation_price( + pair=t.pair, + open_rate=t.open_rate, + is_short=t.is_short, + amount=t.amount, + stake_amount=t.stake_amount, + leverage=trade.leverage, + wallet_balance=total_wallet_stake, + other_trades=[], # TODO: Add other trades + ) + ) + else: + trade.set_liquidation_price( exchange.get_liquidation_price( - pair=t.pair, - open_rate=t.open_rate, - is_short=t.is_short, - amount=t.amount, - stake_amount=t.stake_amount, + pair=trade.pair, + open_rate=trade.open_rate, + is_short=trade.is_short, + amount=trade.amount, + stake_amount=trade.stake_amount, leverage=trade.leverage, - wallet_balance=total_wallet_stake, - other_trades=[], # TODO: Add other trades + wallet_balance=trade.stake_amount, + other_trades=[], ) ) - else: - trade.set_liquidation_price( - exchange.get_liquidation_price( - pair=trade.pair, - open_rate=trade.open_rate, - is_short=trade.is_short, - amount=trade.amount, - stake_amount=trade.stake_amount, - leverage=trade.leverage, - wallet_balance=trade.stake_amount, - other_trades=[], - ) - ) + except DependencyException: + logger.warning("Unable to calculate liquidation price") From 5358f2fb9e9c19bcc26e926b12c99e7a17b1ddc9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 30 Aug 2024 07:23:40 +0200 Subject: [PATCH 09/25] feat: allow liquidation-price update without trades for cross mode --- freqtrade/leverage/liquidation_price.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/freqtrade/leverage/liquidation_price.py b/freqtrade/leverage/liquidation_price.py index 6b51397b5..89d5d1a36 100644 --- a/freqtrade/leverage/liquidation_price.py +++ b/freqtrade/leverage/liquidation_price.py @@ -1,4 +1,5 @@ import logging +from typing import Optional from freqtrade.enums import MarginMode from freqtrade.exceptions import DependencyException @@ -11,7 +12,7 @@ logger = logging.getLogger(__name__) def update_liquidation_prices( - trade: LocalTrade, + trade: Optional[LocalTrade] = None, *, exchange: Exchange, wallets: Wallets, @@ -30,7 +31,8 @@ def update_liquidation_prices( total_wallet_stake = wallets.get_total(stake_currency) logger.info("Updating liquidation price for all open trades.") - for t in Trade.get_open_trades(): + open_trades = Trade.get_open_trades() + for t in open_trades: # TODO: This should be done in a batch update t.set_liquidation_price( exchange.get_liquidation_price( @@ -41,10 +43,10 @@ def update_liquidation_prices( stake_amount=t.stake_amount, leverage=trade.leverage, wallet_balance=total_wallet_stake, - other_trades=[], # TODO: Add other trades + other_trades=[tr for tr in open_trades if t.id != tr.id], ) ) - else: + elif trade: trade.set_liquidation_price( exchange.get_liquidation_price( pair=trade.pair, @@ -57,5 +59,9 @@ def update_liquidation_prices( other_trades=[], ) ) + else: + raise DependencyException( + "Trade object is required for updating liquidation price in isolated margin mode." + ) except DependencyException: logger.warning("Unable to calculate liquidation price") From 45e75f3d090effa18377ce0de4f94c4f5f9d1a1d Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 30 Aug 2024 17:59:22 +0200 Subject: [PATCH 10/25] chore: improve arguments to get_liquidation_price --- freqtrade/exchange/exchange.py | 4 ++-- freqtrade/leverage/liquidation_price.py | 1 - tests/exchange/test_binance.py | 2 -- tests/exchange/test_exchange.py | 4 ---- 4 files changed, 2 insertions(+), 9 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 83aff2def..d6149745a 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -3532,7 +3532,7 @@ class Exchange: stake_amount: float, leverage: float, wallet_balance: float, - other_trades: list, + other_trades: Optional[list] = None, ) -> Optional[float]: """ Set's the margin mode on the exchange to cross or isolated for a specific pair @@ -3554,7 +3554,7 @@ class Exchange: leverage=leverage, stake_amount=stake_amount, wallet_balance=wallet_balance, - other_trades=other_trades, + other_trades=other_trades or [], ) else: positions = self.fetch_positions(pair) diff --git a/freqtrade/leverage/liquidation_price.py b/freqtrade/leverage/liquidation_price.py index 89d5d1a36..e2fb167c8 100644 --- a/freqtrade/leverage/liquidation_price.py +++ b/freqtrade/leverage/liquidation_price.py @@ -56,7 +56,6 @@ def update_liquidation_prices( stake_amount=trade.stake_amount, leverage=trade.leverage, wallet_balance=trade.stake_amount, - other_trades=[], ) ) else: diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index 96f3e6a5b..983a499dd 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -294,8 +294,6 @@ def test_liquidation_price_binance( stake_amount=open_rate * amount, leverage=5, other_trades=other_contracts, - # mm_ex_1=mm_ex_1, - # upnl_ex_1=upnl_ex_1, ), 2, ) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 56a967e5a..7535822c3 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -5524,7 +5524,6 @@ def test_liquidation_price_is_none( stake_amount=open_rate * 71200.81144, leverage=5, wallet_balance=-56354.57, - other_trades=[], ) is None ) @@ -5970,7 +5969,6 @@ def test_get_liquidation_price1(mocker, default_conf): stake_amount=18.884 * 0.8, leverage=leverage, wallet_balance=18.884 * 0.8, - other_trades=[], ) assert liq_price == 17.47 @@ -5984,7 +5982,6 @@ def test_get_liquidation_price1(mocker, default_conf): stake_amount=18.884 * 0.8, leverage=leverage, wallet_balance=18.884 * 0.8, - other_trades=[], ) assert liq_price == 17.540699999999998 @@ -5998,7 +5995,6 @@ def test_get_liquidation_price1(mocker, default_conf): stake_amount=18.884 * 0.8, leverage=leverage, wallet_balance=18.884 * 0.8, - other_trades=[], ) assert liq_price is None default_conf["trading_mode"] = "margin" From 1473abf19af3497d5c9a46801b4d92fc2442305c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 31 Aug 2024 08:20:14 +0200 Subject: [PATCH 11/25] refactor: rename dry-liquidation parameter passing all open trades will be more flexible for the future. --- freqtrade/exchange/binance.py | 6 +++--- freqtrade/exchange/bybit.py | 4 ++-- freqtrade/exchange/exchange.py | 8 ++++---- freqtrade/leverage/liquidation_price.py | 2 +- tests/exchange/test_exchange.py | 4 ++-- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 637bfafef..bb3520cda 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -153,7 +153,7 @@ class Binance(Exchange): stake_amount: float, leverage: float, wallet_balance: float, # Or margin balance - other_trades: list, + open_trades: list, ) -> Optional[float]: """ Important: Must be fetching data from cached values as this is used by backtesting! @@ -171,7 +171,7 @@ class Binance(Exchange): :param wallet_balance: Amount of margin_mode in the wallet being used to trade Cross-Margin Mode: crossWalletBalance Isolated-Margin Mode: isolatedWalletBalance - :param other_trades: List of other open trades in the same wallet + :param open_trades: List of open trades in the same wallet # * Only required for Cross :param mm_ex_1: (TMM) @@ -191,7 +191,7 @@ class Binance(Exchange): if self.margin_mode == MarginMode.CROSS: mm_ex_1: float = 0.0 upnl_ex_1: float = 0.0 - for trade in other_trades: + for trade in open_trades: mm_ratio1, maint_amnt1 = self.get_maintenance_ratio_and_amt( trade["pair"], trade["stake_amount"] ) diff --git a/freqtrade/exchange/bybit.py b/freqtrade/exchange/bybit.py index c14273e53..967c80b7d 100644 --- a/freqtrade/exchange/bybit.py +++ b/freqtrade/exchange/bybit.py @@ -147,7 +147,7 @@ class Bybit(Exchange): stake_amount: float, leverage: float, wallet_balance: float, # Or margin balance - other_trades: list, + open_trades: list, ) -> Optional[float]: """ Important: Must be fetching data from cached values as this is used by backtesting! @@ -177,7 +177,7 @@ class Bybit(Exchange): :param wallet_balance: Amount of margin_mode in the wallet being used to trade Cross-Margin Mode: crossWalletBalance Isolated-Margin Mode: isolatedWalletBalance - :param other_trades: List of other open trades in the same wallet + :param open_trades: List of other open trades in the same wallet """ market = self.markets[pair] diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index d6149745a..c0b4a72e9 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -3532,7 +3532,7 @@ class Exchange: stake_amount: float, leverage: float, wallet_balance: float, - other_trades: Optional[list] = None, + open_trades: Optional[list] = None, ) -> Optional[float]: """ Set's the margin mode on the exchange to cross or isolated for a specific pair @@ -3554,7 +3554,7 @@ class Exchange: leverage=leverage, stake_amount=stake_amount, wallet_balance=wallet_balance, - other_trades=other_trades or [], + open_trades=open_trades or [], ) else: positions = self.fetch_positions(pair) @@ -3580,7 +3580,7 @@ class Exchange: stake_amount: float, leverage: float, wallet_balance: float, # Or margin balance - other_trades: list, + open_trades: list, ) -> Optional[float]: """ Important: Must be fetching data from cached values as this is used by backtesting! @@ -3605,7 +3605,7 @@ class Exchange: :param wallet_balance: Amount of margin_mode in the wallet being used to trade Cross-Margin Mode: crossWalletBalance Isolated-Margin Mode: isolatedWalletBalance - :param other_trades: List of other open trades in the same wallet + :param open_trades: List of other open trades in the same wallet """ market = self.markets[pair] diff --git a/freqtrade/leverage/liquidation_price.py b/freqtrade/leverage/liquidation_price.py index e2fb167c8..027cd74a6 100644 --- a/freqtrade/leverage/liquidation_price.py +++ b/freqtrade/leverage/liquidation_price.py @@ -43,7 +43,7 @@ def update_liquidation_prices( stake_amount=t.stake_amount, leverage=trade.leverage, wallet_balance=total_wallet_stake, - other_trades=[tr for tr in open_trades if t.id != tr.id], + open_trades=[tr for tr in open_trades], ) ) elif trade: diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 7535822c3..35d0b18dd 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -6009,7 +6009,7 @@ def test_get_liquidation_price1(mocker, default_conf): stake_amount=18.884 * 0.8, leverage=leverage, wallet_balance=18.884 * 0.8, - other_trades=[], + open_trades=[], ) @@ -6140,7 +6140,7 @@ def test_get_liquidation_price( wallet_balance=amount * open_rate / leverage, leverage=leverage, is_short=is_short, - other_trades=[], + open_trades=[], ) if expected_liq is None: assert liq is None From 0d5919392e86f63cb92f9de554b84fbc998f1320 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 31 Aug 2024 08:20:51 +0200 Subject: [PATCH 12/25] test: update binance test --- tests/exchange/test_binance.py | 45 ++++++++++++++++++++++++++-------- 1 file changed, 35 insertions(+), 10 deletions(-) diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index 983a499dd..269b616d3 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -171,11 +171,12 @@ def test_stoploss_adjust_binance(mocker, default_conf, sl1, sl2, sl3, side): @pytest.mark.parametrize( - "is_short, trading_mode, margin_mode, wallet_balance, " - "maintenance_amt, amount, open_rate, mark_price, other_contracts," + "pair, is_short, trading_mode, margin_mode, wallet_balance, " + "maintenance_amt, amount, open_rate, mark_price, open_trades," "mm_ratio, expected", [ ( + "ETH/USDT:USDT", False, "futures", "isolated", @@ -189,6 +190,7 @@ def test_stoploss_adjust_binance(mocker, default_conf, sl1, sl2, sl3, side): 1114.78, ), ( + "ETH/USDT:USDT", False, "futures", "isolated", @@ -202,6 +204,7 @@ def test_stoploss_adjust_binance(mocker, default_conf, sl1, sl2, sl3, side): 18778.73, ), ( + "ETH/USDT:USDT", False, "futures", "cross", @@ -222,12 +225,23 @@ def test_stoploss_adjust_binance(mocker, default_conf, sl1, sl2, sl3, side): "mark_price": 31967.27, "mm_ratio": 0.025, "maintenance_amt": 16300.0, - } + }, + { + # From calc example + "pair": "ETH/USDT:USDT", + "open_rate": 1456.84, + "amount": 3683.979, + "stake_amount": 5366967.96, + "mark_price": 1335.18, + "mm_ratio": 0.10, + "maintenance_amt": 135365.00, + }, ], 0.10, 1153.26, ), ( + "BTC/USDT:USDT", False, "futures", "cross", @@ -239,6 +253,16 @@ def test_stoploss_adjust_binance(mocker, default_conf, sl1, sl2, sl3, side): 32481.980, # open_rate 31967.27, # mark_price [ + { + # From calc example + "pair": "BTC/USDT:USDT", + "open_rate": 32481.98, + "amount": 109.488, + "stake_amount": 3556387.02624, # open_rate * amount + "mark_price": 31967.27, + "mm_ratio": 0.025, + "maintenance_amt": 16300.0, + }, { # From calc example "pair": "ETH/USDT:USDT", @@ -248,7 +272,7 @@ def test_stoploss_adjust_binance(mocker, default_conf, sl1, sl2, sl3, side): "mark_price": 1335.18, "mm_ratio": 0.10, "maintenance_amt": 135365.00, - } + }, ], 0.025, 26316.89, @@ -258,6 +282,7 @@ def test_stoploss_adjust_binance(mocker, default_conf, sl1, sl2, sl3, side): def test_liquidation_price_binance( mocker, default_conf, + pair, is_short, trading_mode, margin_mode, @@ -266,7 +291,7 @@ def test_liquidation_price_binance( amount, open_rate, mark_price, - other_contracts, + open_trades, mm_ratio, expected, ): @@ -275,9 +300,9 @@ def test_liquidation_price_binance( default_conf["liquidation_buffer"] = 0.0 exchange = get_patched_exchange(mocker, default_conf, exchange="binance") - def get_maint_ratio(pair, stake_amount): - if pair != "DOGE/USDT": - oc = [c for c in other_contracts if c["pair"] == pair][0] + def get_maint_ratio(pair_, stake_amount): + if pair_ != pair: + oc = [c for c in open_trades if c["pair"] == pair_][0] return oc["mm_ratio"], oc["maintenance_amt"] return mm_ratio, maintenance_amt @@ -286,14 +311,14 @@ def test_liquidation_price_binance( pytest.approx( round( exchange.get_liquidation_price( - pair="DOGE/USDT", + pair=pair, open_rate=open_rate, is_short=is_short, wallet_balance=wallet_balance, amount=amount, stake_amount=open_rate * amount, leverage=5, - other_trades=other_contracts, + open_trades=open_trades, ), 2, ) From ac8bc7dec210f1f51dbe2ba5d09a2d99907e2f44 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 31 Aug 2024 08:21:33 +0200 Subject: [PATCH 13/25] fix: use "other trades" logic for binance cross calc --- freqtrade/exchange/binance.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index bb3520cda..027888fb1 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -192,6 +192,9 @@ class Binance(Exchange): mm_ex_1: float = 0.0 upnl_ex_1: float = 0.0 for trade in open_trades: + if trade["pair"] == pair: + # Only "other" trades are considered + continue mm_ratio1, maint_amnt1 = self.get_maintenance_ratio_and_amt( trade["pair"], trade["stake_amount"] ) From 8bf314202f98521b73823a23f7779c85fbcc3021 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 31 Aug 2024 16:35:47 +0200 Subject: [PATCH 14/25] chore: simplify call to liquidation price for cross futures --- freqtrade/leverage/liquidation_price.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/leverage/liquidation_price.py b/freqtrade/leverage/liquidation_price.py index 027cd74a6..03ac4af0f 100644 --- a/freqtrade/leverage/liquidation_price.py +++ b/freqtrade/leverage/liquidation_price.py @@ -43,7 +43,7 @@ def update_liquidation_prices( stake_amount=t.stake_amount, leverage=trade.leverage, wallet_balance=total_wallet_stake, - open_trades=[tr for tr in open_trades], + open_trades=open_trades, ) ) elif trade: From fe7a88362b7c585a7da3d6cb3410d17daa4d19dc Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 31 Aug 2024 16:43:34 +0200 Subject: [PATCH 15/25] feat: add method to fetch binance funding fees which is necessary to calculate accurate liquidation prices --- freqtrade/exchange/binance.py | 30 ++++++++++++++++++++++++++---- tests/exchange/test_binance.py | 18 ++++++++++++------ 2 files changed, 38 insertions(+), 10 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 027888fb1..3ef6be74c 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -144,6 +144,27 @@ class Binance(Exchange): """ return open_date.minute == 0 and open_date.second < 15 + def fetch_funding_rates(self, symbols: Optional[List[str]] = None) -> Dict[str, float]: + """ + Fetch funding rates for the given symbols. + :param symbols: List of symbols to fetch funding rates for + :return: Dict of funding rates for the given symbols + """ + try: + if self.trading_mode == TradingMode.FUTURES: + rates = self._api.fetch_funding_rates(symbols) + return rates + return {} + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e + except (ccxt.OperationFailed, ccxt.ExchangeError) as e: + raise TemporaryError( + f"Error in additional_exchange_init due to {e.__class__.__name__}. Message: {e}" + ) from e + + except ccxt.BaseError as e: + raise OperationalException(e) from e + def dry_run_liquidation_price( self, pair: str, @@ -191,19 +212,20 @@ class Binance(Exchange): if self.margin_mode == MarginMode.CROSS: mm_ex_1: float = 0.0 upnl_ex_1: float = 0.0 + pairs = [trade["pair"] for trade in open_trades] + funding_rates = self.fetch_funding_rates(pairs) for trade in open_trades: if trade["pair"] == pair: # Only "other" trades are considered continue + mark_price = funding_rates[trade["pair"]]["markPrice"] mm_ratio1, maint_amnt1 = self.get_maintenance_ratio_and_amt( trade["pair"], trade["stake_amount"] ) - maint_margin = trade["amount"] * trade["mark_price"] * mm_ratio1 - maint_amnt1 + maint_margin = trade["amount"] * mark_price * mm_ratio1 - maint_amnt1 mm_ex_1 += maint_margin - upnl_ex_1 += ( - trade["amount"] * trade["mark_price"] - trade["amount"] * trade["open_rate"] - ) + upnl_ex_1 += trade["amount"] * mark_price - trade["amount"] * trade["open_rate"] cross_vars = upnl_ex_1 - mm_ex_1 side_1 = -1 if is_short else 1 diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index 269b616d3..63c7dfa00 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -172,7 +172,7 @@ def test_stoploss_adjust_binance(mocker, default_conf, sl1, sl2, sl3, side): @pytest.mark.parametrize( "pair, is_short, trading_mode, margin_mode, wallet_balance, " - "maintenance_amt, amount, open_rate, mark_price, open_trades," + "maintenance_amt, amount, open_rate, open_trades," "mm_ratio, expected", [ ( @@ -184,7 +184,6 @@ def test_stoploss_adjust_binance(mocker, default_conf, sl1, sl2, sl3, side): 135365.00, 3683.979, 1456.84, - 1456.84, # mark price [], 0.10, 1114.78, @@ -198,7 +197,6 @@ def test_stoploss_adjust_binance(mocker, default_conf, sl1, sl2, sl3, side): 16300.000, 109.488, 32481.980, - 32481.980, [], 0.025, 18778.73, @@ -214,7 +212,6 @@ def test_stoploss_adjust_binance(mocker, default_conf, sl1, sl2, sl3, side): 135365.00, 3683.979, # amount 1456.84, # open_rate - 1335.18, # mark_price [ { # From calc example @@ -251,7 +248,6 @@ def test_stoploss_adjust_binance(mocker, default_conf, sl1, sl2, sl3, side): 16300.0, 109.488, # amount 32481.980, # open_rate - 31967.27, # mark_price [ { # From calc example @@ -290,7 +286,6 @@ def test_liquidation_price_binance( maintenance_amt, amount, open_rate, - mark_price, open_trades, mm_ratio, expected, @@ -306,7 +301,18 @@ def test_liquidation_price_binance( return oc["mm_ratio"], oc["maintenance_amt"] return mm_ratio, maintenance_amt + def fetch_funding_rates(*args, **kwargs): + return { + t["pair"]: { + "symbol": t["pair"], + "markPrice": t["mark_price"], + } + for t in open_trades + } + exchange.get_maintenance_ratio_and_amt = get_maint_ratio + exchange.fetch_funding_rates = fetch_funding_rates + assert ( pytest.approx( round( From 319e8d746fd8c6b99c90f79eb31afa88ac2355d2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 31 Aug 2024 16:46:39 +0200 Subject: [PATCH 16/25] feat: use proper trade objects for liquidation calc --- freqtrade/exchange/binance.py | 12 ++++++------ tests/exchange/test_binance.py | 14 +++++++++++++- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 3ef6be74c..a837068d1 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -212,20 +212,20 @@ class Binance(Exchange): if self.margin_mode == MarginMode.CROSS: mm_ex_1: float = 0.0 upnl_ex_1: float = 0.0 - pairs = [trade["pair"] for trade in open_trades] + pairs = [trade.pair for trade in open_trades] funding_rates = self.fetch_funding_rates(pairs) for trade in open_trades: - if trade["pair"] == pair: + if trade.pair == pair: # Only "other" trades are considered continue - mark_price = funding_rates[trade["pair"]]["markPrice"] + mark_price = funding_rates[trade.pair]["markPrice"] mm_ratio1, maint_amnt1 = self.get_maintenance_ratio_and_amt( - trade["pair"], trade["stake_amount"] + trade.pair, trade.stake_amount ) - maint_margin = trade["amount"] * mark_price * mm_ratio1 - maint_amnt1 + maint_margin = trade.amount * mark_price * mm_ratio1 - maint_amnt1 mm_ex_1 += maint_margin - upnl_ex_1 += trade["amount"] * mark_price - trade["amount"] * trade["open_rate"] + upnl_ex_1 += trade.amount * mark_price - trade.amount * trade.open_rate cross_vars = upnl_ex_1 - mm_ex_1 side_1 = -1 if is_short else 1 diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index 63c7dfa00..956f0b850 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -7,6 +7,7 @@ import pytest from freqtrade.enums import CandleType, MarginMode, TradingMode from freqtrade.exceptions import DependencyException, InvalidOrderException, OperationalException +from freqtrade.persistence.trade_model import Trade from tests.conftest import EXMS, get_mock_coro, get_patched_exchange, log_has_re from tests.exchange.test_exchange import ccxt_exceptionhandlers @@ -313,6 +314,17 @@ def test_liquidation_price_binance( exchange.get_maintenance_ratio_and_amt = get_maint_ratio exchange.fetch_funding_rates = fetch_funding_rates + open_trade_objects = [ + Trade( + pair=t["pair"], + open_rate=t["open_rate"], + amount=t["amount"], + stake_amount=t["stake_amount"], + fee_open=0, + ) + for t in open_trades + ] + assert ( pytest.approx( round( @@ -324,7 +336,7 @@ def test_liquidation_price_binance( amount=amount, stake_amount=open_rate * amount, leverage=5, - open_trades=open_trades, + open_trades=open_trade_objects, ), 2, ) From 4d40ffedff0de315d0a33a05d36d2fd85e78ef32 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 31 Aug 2024 17:37:08 +0200 Subject: [PATCH 17/25] fix: allow setting 0 as liquidation price --- freqtrade/persistence/trade_model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 698e9721c..5ff42ed64 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -760,7 +760,7 @@ class LocalTrade: Method you should use to set self.liquidation price. Assures stop_loss is not passed the liquidation price """ - if not liquidation_price: + if liquidation_price is None: return self.liquidation_price = liquidation_price From cba6bd6ef55090f706d72c44a142f99051bc5689 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 31 Aug 2024 17:40:25 +0200 Subject: [PATCH 18/25] fix: use t.leverage, not trade.leverage for cross liq calculations --- freqtrade/leverage/liquidation_price.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/leverage/liquidation_price.py b/freqtrade/leverage/liquidation_price.py index 03ac4af0f..af6ef0d44 100644 --- a/freqtrade/leverage/liquidation_price.py +++ b/freqtrade/leverage/liquidation_price.py @@ -41,7 +41,7 @@ def update_liquidation_prices( is_short=t.is_short, amount=t.amount, stake_amount=t.stake_amount, - leverage=trade.leverage, + leverage=t.leverage, wallet_balance=total_wallet_stake, open_trades=open_trades, ) From 36ae564d2611c3e25d2d225c82778f58e0cef6fb Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 31 Aug 2024 20:25:24 +0200 Subject: [PATCH 19/25] feat: update liquidation price on startup --- freqtrade/freqtradebot.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 5097722fe..ac132bea5 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -22,6 +22,7 @@ from freqtrade.edge import Edge from freqtrade.enums import ( ExitCheckTuple, ExitType, + MarginMode, RPCMessageType, SignalDirection, State, @@ -241,6 +242,7 @@ class FreqtradeBot(LoggingMixin): # Only update open orders on startup # This will update the database after the initial migration self.startup_update_open_orders() + self.update_all_liquidation_prices() self.update_funding_fees() def process(self) -> None: @@ -357,6 +359,16 @@ class FreqtradeBot(LoggingMixin): open_trades = Trade.get_open_trade_count() return max(0, self.config["max_open_trades"] - open_trades) + def update_all_liquidation_prices(self) -> None: + if self.trading_mode == TradingMode.FUTURES and self.margin_mode == MarginMode.CROSS: + # Update liquidation prices for all trades in cross margin mode + update_liquidation_prices( + exchange=self.exchange, + wallets=self.wallets, + stake_currency=self.config["stake_currency"], + dry_run=self.config["dry_run"], + ) + def update_funding_fees(self) -> None: if self.trading_mode == TradingMode.FUTURES: trades: List[Trade] = Trade.get_open_trades() From c5525d356ec8b4702337f2b7cff3410465e44c03 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 5 Sep 2024 18:06:22 +0200 Subject: [PATCH 20/25] feat: support backtesting with cross configuration --- freqtrade/exchange/binance.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index a837068d1..a2aac6fb2 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -213,12 +213,17 @@ class Binance(Exchange): mm_ex_1: float = 0.0 upnl_ex_1: float = 0.0 pairs = [trade.pair for trade in open_trades] - funding_rates = self.fetch_funding_rates(pairs) + if self._config["runmode"] in ("live", "dry_run"): + funding_rates = self.fetch_funding_rates(pairs) for trade in open_trades: if trade.pair == pair: # Only "other" trades are considered continue - mark_price = funding_rates[trade.pair]["markPrice"] + if self._config["runmode"] in ("live", "dry_run"): + mark_price = funding_rates[trade.pair]["markPrice"] + else: + # Fall back to open rate for backtesting + mark_price = trade.open_rate mm_ratio1, maint_amnt1 = self.get_maintenance_ratio_and_amt( trade.pair, trade.stake_amount ) @@ -226,6 +231,7 @@ class Binance(Exchange): mm_ex_1 += maint_margin upnl_ex_1 += trade.amount * mark_price - trade.amount * trade.open_rate + cross_vars = upnl_ex_1 - mm_ex_1 side_1 = -1 if is_short else 1 From abe01f8f48556c5efc5d6ca2fc0387f5cd19b412 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 5 Sep 2024 18:07:01 +0200 Subject: [PATCH 21/25] feat: implement liquidation price update on all order fills --- freqtrade/optimize/backtesting.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 0d7967762..7d0428939 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -26,6 +26,7 @@ from freqtrade.enums import ( CandleType, ExitCheckTuple, ExitType, + MarginMode, RunMode, TradingMode, ) @@ -207,6 +208,7 @@ class Backtesting: self.required_startup = self.dataprovider.get_required_startup(self.timeframe) self.trading_mode: TradingMode = config.get("trading_mode", TradingMode.SPOT) + self.margin_mode: MarginMode = config.get("margin_mode", MarginMode.ISOLATED) # strategies which define "can_short=True" will fail to load in Spot mode. self._can_short = self.trading_mode != TradingMode.SPOT self._position_stacking: bool = self.config.get("position_stacking", False) @@ -699,8 +701,11 @@ class Backtesting: current_time=current_date, ) - if not (order.ft_order_side == trade.exit_side and order.safe_amount == trade.amount): - # trade is still open + if self.margin_mode == MarginMode.CROSS or not ( + order.ft_order_side == trade.exit_side and order.safe_amount == trade.amount + ): + # trade is still open or we are in cross margin mode and + # must update all liquidation prices update_liquidation_prices( trade, exchange=self.exchange, @@ -708,8 +713,8 @@ class Backtesting: stake_currency=self.config["stake_currency"], dry_run=self.config["dry_run"], ) + if not (order.ft_order_side == trade.exit_side and order.safe_amount == trade.amount): self._call_adjust_stop(current_date, trade, order.ft_price) - # pass return True return False From 9bdee1b82d015304e605ea2f9a0e30e55298a201 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 6 Sep 2024 07:10:15 +0200 Subject: [PATCH 22/25] feat: improve typing of fetch_funding_rates --- freqtrade/exchange/binance.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index a2aac6fb2..13582b183 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -144,7 +144,9 @@ class Binance(Exchange): """ return open_date.minute == 0 and open_date.second < 15 - def fetch_funding_rates(self, symbols: Optional[List[str]] = None) -> Dict[str, float]: + def fetch_funding_rates( + self, symbols: Optional[List[str]] = None + ) -> Dict[str, Dict[str, float]]: """ Fetch funding rates for the given symbols. :param symbols: List of symbols to fetch funding rates for From 86721b88cefddd0997acf40c9e18301a63f9c5bf Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 2 Oct 2024 06:46:37 +0200 Subject: [PATCH 23/25] chore: improve import logic --- tests/exchange/test_binance.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index 956f0b850..6ebfc2109 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -7,7 +7,7 @@ import pytest from freqtrade.enums import CandleType, MarginMode, TradingMode from freqtrade.exceptions import DependencyException, InvalidOrderException, OperationalException -from freqtrade.persistence.trade_model import Trade +from freqtrade.persistence import Trade from tests.conftest import EXMS, get_mock_coro, get_patched_exchange, log_has_re from tests.exchange.test_exchange import ccxt_exceptionhandlers From 9ba0c54295b474f4d5e5a5b14e8f0b810a5ceba3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 2 Oct 2024 07:04:26 +0200 Subject: [PATCH 24/25] chore: cleanup test code --- tests/exchange/test_binance.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index 6ebfc2109..623a9f17a 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -208,8 +208,6 @@ def test_stoploss_adjust_binance(mocker, default_conf, sl1, sl2, sl3, side): "futures", "cross", 1535443.01, - # 71200.81144, # tmm1 - # -56354.57, # upnl1 135365.00, 3683.979, # amount 1456.84, # open_rate @@ -244,8 +242,6 @@ def test_stoploss_adjust_binance(mocker, default_conf, sl1, sl2, sl3, side): "futures", "cross", 1535443.01, - # 356512.508, # tmm1 - # -448192.89, # upnl1 16300.0, 109.488, # amount 32481.980, # open_rate From a0912ad6b42babe53f7167a6d00826ab81fbf293 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 2 Oct 2024 18:10:28 +0200 Subject: [PATCH 25/25] tests: update ccxt compat test --- tests/exchange_online/test_ccxt_compat.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/exchange_online/test_ccxt_compat.py b/tests/exchange_online/test_ccxt_compat.py index fbb6bca05..32133d8e5 100644 --- a/tests/exchange_online/test_ccxt_compat.py +++ b/tests/exchange_online/test_ccxt_compat.py @@ -457,6 +457,7 @@ class TestCCXTExchange: stake_amount=100, leverage=5, wallet_balance=100, + open_trades=[], ) assert isinstance(liquidation_price, float) assert liquidation_price >= 0.0 @@ -469,6 +470,7 @@ class TestCCXTExchange: stake_amount=100, leverage=5, wallet_balance=100, + open_trades=[], ) assert isinstance(liquidation_price, float) assert liquidation_price >= 0.0