Merge pull request #10741 from freqtrade/feat/improve_liquidation_logic

Improved liquidation price logic
This commit is contained in:
Matthias
2024-10-02 19:49:03 +02:00
committed by GitHub
11 changed files with 315 additions and 69 deletions

View File

@@ -144,6 +144,29 @@ class Binance(Exchange):
""" """
return open_date.minute == 0 and open_date.second < 15 return open_date.minute == 0 and open_date.second < 15
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
: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( def dry_run_liquidation_price(
self, self,
pair: str, pair: str,
@@ -153,8 +176,7 @@ class Binance(Exchange):
stake_amount: float, stake_amount: float,
leverage: float, leverage: float,
wallet_balance: float, # Or margin balance wallet_balance: float, # Or margin balance
mm_ex_1: float = 0.0, # (Binance) Cross only open_trades: list,
upnl_ex_1: float = 0.0, # (Binance) Cross only
) -> Optional[float]: ) -> Optional[float]:
""" """
Important: Must be fetching data from cached values as this is used by backtesting! Important: Must be fetching data from cached values as this is used by backtesting!
@@ -172,6 +194,7 @@ class Binance(Exchange):
:param wallet_balance: Amount of margin_mode in the wallet being used to trade :param wallet_balance: Amount of margin_mode in the wallet being used to trade
Cross-Margin Mode: crossWalletBalance Cross-Margin Mode: crossWalletBalance
Isolated-Margin Mode: isolatedWalletBalance Isolated-Margin Mode: isolatedWalletBalance
:param open_trades: List of open trades in the same wallet
# * Only required for Cross # * Only required for Cross
:param mm_ex_1: (TMM) :param mm_ex_1: (TMM)
@@ -180,15 +203,41 @@ class Binance(Exchange):
:param upnl_ex_1: (UPNL) :param upnl_ex_1: (UPNL)
Cross-Margin Mode: Unrealized PNL of all other contracts, excluding Contract 1. Cross-Margin Mode: Unrealized PNL of all other contracts, excluding Contract 1.
Isolated-Margin Mode: 0 Isolated-Margin Mode: 0
:param other
""" """
cross_vars: float = 0.0
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
# mm_ratio: Binance's formula specifies maintenance margin rate which is mm_ratio * 100% # mm_ratio: Binance's formula specifies maintenance margin rate which is mm_ratio * 100%
# maintenance_amt: (CUM) Maintenance Amount of position # maintenance_amt: (CUM) Maintenance Amount of position
mm_ratio, maintenance_amt = self.get_maintenance_ratio_and_amt(pair, stake_amount) 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
pairs = [trade.pair for trade in open_trades]
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
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
)
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
cross_vars = upnl_ex_1 - mm_ex_1
side_1 = -1 if is_short else 1
if maintenance_amt is None: if maintenance_amt is None:
raise OperationalException( raise OperationalException(
"Parameter maintenance_amt is required by Binance.liquidation_price" "Parameter maintenance_amt is required by Binance.liquidation_price"

View File

@@ -147,8 +147,7 @@ class Bybit(Exchange):
stake_amount: float, stake_amount: float,
leverage: float, leverage: float,
wallet_balance: float, # Or margin balance wallet_balance: float, # Or margin balance
mm_ex_1: float = 0.0, # (Binance) Cross only open_trades: list,
upnl_ex_1: float = 0.0, # (Binance) Cross only
) -> Optional[float]: ) -> Optional[float]:
""" """
Important: Must be fetching data from cached values as this is used by backtesting! 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 :param wallet_balance: Amount of margin_mode in the wallet being used to trade
Cross-Margin Mode: crossWalletBalance Cross-Margin Mode: crossWalletBalance
Isolated-Margin Mode: isolatedWalletBalance Isolated-Margin Mode: isolatedWalletBalance
:param open_trades: List of other open trades in the same wallet
""" """
market = self.markets[pair] market = self.markets[pair]

View File

@@ -3532,8 +3532,7 @@ class Exchange:
stake_amount: float, stake_amount: float,
leverage: float, leverage: float,
wallet_balance: float, wallet_balance: float,
mm_ex_1: float = 0.0, # (Binance) Cross only open_trades: Optional[list] = None,
upnl_ex_1: float = 0.0, # (Binance) Cross only
) -> Optional[float]: ) -> Optional[float]:
""" """
Set's the margin mode on the exchange to cross or isolated for a specific pair Set's the margin mode on the exchange to cross or isolated for a specific pair
@@ -3555,8 +3554,7 @@ class Exchange:
leverage=leverage, leverage=leverage,
stake_amount=stake_amount, stake_amount=stake_amount,
wallet_balance=wallet_balance, wallet_balance=wallet_balance,
mm_ex_1=mm_ex_1, open_trades=open_trades or [],
upnl_ex_1=upnl_ex_1,
) )
else: else:
positions = self.fetch_positions(pair) positions = self.fetch_positions(pair)
@@ -3582,8 +3580,7 @@ class Exchange:
stake_amount: float, stake_amount: float,
leverage: float, leverage: float,
wallet_balance: float, # Or margin balance wallet_balance: float, # Or margin balance
mm_ex_1: float = 0.0, # (Binance) Cross only open_trades: list,
upnl_ex_1: float = 0.0, # (Binance) Cross only
) -> Optional[float]: ) -> Optional[float]:
""" """
Important: Must be fetching data from cached values as this is used by backtesting! 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 :param wallet_balance: Amount of margin_mode in the wallet being used to trade
Cross-Margin Mode: crossWalletBalance Cross-Margin Mode: crossWalletBalance
Isolated-Margin Mode: isolatedWalletBalance Isolated-Margin Mode: isolatedWalletBalance
:param open_trades: List of other open trades in the same wallet
# * Not required by Gate or OKX
:param mm_ex_1:
:param upnl_ex_1:
""" """
market = self.markets[pair] market = self.markets[pair]

View File

@@ -43,6 +43,7 @@ from freqtrade.exchange import (
timeframe_to_next_date, timeframe_to_next_date,
timeframe_to_seconds, timeframe_to_seconds,
) )
from freqtrade.leverage.liquidation_price import update_liquidation_prices
from freqtrade.misc import safe_value_fallback, safe_value_fallback2 from freqtrade.misc import safe_value_fallback, safe_value_fallback2
from freqtrade.mixins import LoggingMixin from freqtrade.mixins import LoggingMixin
from freqtrade.persistence import Order, PairLocks, Trade, init_db from freqtrade.persistence import Order, PairLocks, Trade, init_db
@@ -241,6 +242,7 @@ class FreqtradeBot(LoggingMixin):
# Only update open orders on startup # Only update open orders on startup
# This will update the database after the initial migration # This will update the database after the initial migration
self.startup_update_open_orders() self.startup_update_open_orders()
self.update_all_liquidation_prices()
self.update_funding_fees() self.update_funding_fees()
def process(self) -> None: def process(self) -> None:
@@ -357,6 +359,16 @@ class FreqtradeBot(LoggingMixin):
open_trades = Trade.get_open_trade_count() open_trades = Trade.get_open_trade_count()
return max(0, self.config["max_open_trades"] - open_trades) 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: def update_funding_fees(self) -> None:
if self.trading_mode == TradingMode.FUTURES: if self.trading_mode == TradingMode.FUTURES:
trades: List[Trade] = Trade.get_open_trades() trades: List[Trade] = Trade.get_open_trades()
@@ -2233,20 +2245,13 @@ class FreqtradeBot(LoggingMixin):
# Must also run for partial exits # Must also run for partial exits
# TODO: Margin will need to use interest_rate as well. # TODO: Margin will need to use interest_rate as well.
# interest_rate = self.exchange.get_interest_rate() # interest_rate = self.exchange.get_interest_rate()
try: update_liquidation_prices(
trade.set_liquidation_price( trade,
self.exchange.get_liquidation_price( exchange=self.exchange,
pair=trade.pair, wallets=self.wallets,
open_rate=trade.open_rate, stake_currency=self.config["stake_currency"],
is_short=trade.is_short, dry_run=self.config["dry_run"],
amount=trade.amount, )
stake_amount=trade.stake_amount,
leverage=trade.leverage,
wallet_balance=trade.stake_amount,
)
)
except DependencyException:
logger.warning("Unable to calculate liquidation price")
if self.strategy.use_custom_stoploss: if self.strategy.use_custom_stoploss:
current_rate = self.exchange.get_rate( current_rate = self.exchange.get_rate(
trade.pair, side="exit", is_short=trade.is_short, refresh=True trade.pair, side="exit", is_short=trade.is_short, refresh=True

View File

@@ -0,0 +1,66 @@
import logging
from typing import Optional
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
logger = logging.getLogger(__name__)
def update_liquidation_prices(
trade: Optional[LocalTrade] = None,
*,
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.
"""
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.")
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(
pair=t.pair,
open_rate=t.open_rate,
is_short=t.is_short,
amount=t.amount,
stake_amount=t.stake_amount,
leverage=t.leverage,
wallet_balance=total_wallet_stake,
open_trades=open_trades,
)
)
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,
)
)
else:
raise DependencyException(
"Trade object is required for updating liquidation price in isolated margin mode."
)
except DependencyException:
logger.warning("Unable to calculate liquidation price")

View File

@@ -26,6 +26,7 @@ from freqtrade.enums import (
CandleType, CandleType,
ExitCheckTuple, ExitCheckTuple,
ExitType, ExitType,
MarginMode,
RunMode, RunMode,
TradingMode, TradingMode,
) )
@@ -37,6 +38,7 @@ from freqtrade.exchange import (
) )
from freqtrade.exchange.exchange import Exchange from freqtrade.exchange.exchange import Exchange
from freqtrade.ft_types import BacktestResultType, get_BacktestResultType_default 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.mixins import LoggingMixin
from freqtrade.optimize.backtest_caching import get_strategy_run_id from freqtrade.optimize.backtest_caching import get_strategy_run_id
from freqtrade.optimize.bt_progress import BTProgress from freqtrade.optimize.bt_progress import BTProgress
@@ -206,6 +208,7 @@ class Backtesting:
self.required_startup = self.dataprovider.get_required_startup(self.timeframe) self.required_startup = self.dataprovider.get_required_startup(self.timeframe)
self.trading_mode: TradingMode = config.get("trading_mode", TradingMode.SPOT) 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. # strategies which define "can_short=True" will fail to load in Spot mode.
self._can_short = self.trading_mode != TradingMode.SPOT self._can_short = self.trading_mode != TradingMode.SPOT
self._position_stacking: bool = self.config.get("position_stacking", False) self._position_stacking: bool = self.config.get("position_stacking", False)
@@ -698,21 +701,20 @@ class Backtesting:
current_time=current_date, current_time=current_date,
) )
if not (order.ft_order_side == trade.exit_side and order.safe_amount == trade.amount): if self.margin_mode == MarginMode.CROSS or not (
# trade is still open order.ft_order_side == trade.exit_side and order.safe_amount == trade.amount
trade.set_liquidation_price( ):
self.exchange.get_liquidation_price( # trade is still open or we are in cross margin mode and
pair=trade.pair, # must update all liquidation prices
open_rate=trade.open_rate, update_liquidation_prices(
is_short=trade.is_short, trade,
amount=trade.amount, exchange=self.exchange,
stake_amount=trade.stake_amount, wallets=self.wallets,
leverage=trade.leverage, stake_currency=self.config["stake_currency"],
wallet_balance=trade.stake_amount, 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) self._call_adjust_stop(current_date, trade, order.ft_price)
# pass
return True return True
return False return False

View File

@@ -760,7 +760,7 @@ class LocalTrade:
Method you should use to set self.liquidation price. Method you should use to set self.liquidation price.
Assures stop_loss is not passed the liquidation price Assures stop_loss is not passed the liquidation price
""" """
if not liquidation_price: if liquidation_price is None:
return return
self.liquidation_price = liquidation_price self.liquidation_price = liquidation_price

View File

@@ -7,6 +7,7 @@ import pytest
from freqtrade.enums import CandleType, MarginMode, TradingMode from freqtrade.enums import CandleType, MarginMode, TradingMode
from freqtrade.exceptions import DependencyException, InvalidOrderException, OperationalException from freqtrade.exceptions import DependencyException, InvalidOrderException, OperationalException
from freqtrade.persistence import Trade
from tests.conftest import EXMS, get_mock_coro, get_patched_exchange, log_has_re from tests.conftest import EXMS, get_mock_coro, get_patched_exchange, log_has_re
from tests.exchange.test_exchange import ccxt_exceptionhandlers from tests.exchange.test_exchange import ccxt_exceptionhandlers
@@ -171,59 +172,101 @@ def test_stoploss_adjust_binance(mocker, default_conf, sl1, sl2, sl3, side):
@pytest.mark.parametrize( @pytest.mark.parametrize(
"is_short, trading_mode, margin_mode, wallet_balance, " "pair, is_short, trading_mode, margin_mode, wallet_balance, "
"mm_ex_1, upnl_ex_1, maintenance_amt, amount, open_rate, " "maintenance_amt, amount, open_rate, open_trades,"
"mm_ratio, expected", "mm_ratio, expected",
[ [
( (
"ETH/USDT:USDT",
False, False,
"futures", "futures",
"isolated", "isolated",
1535443.01, 1535443.01,
0.0,
0.0,
135365.00, 135365.00,
3683.979, 3683.979,
1456.84, 1456.84,
[],
0.10, 0.10,
1114.78, 1114.78,
), ),
( (
"ETH/USDT:USDT",
False, False,
"futures", "futures",
"isolated", "isolated",
1535443.01, 1535443.01,
0.0,
0.0,
16300.000, 16300.000,
109.488, 109.488,
32481.980, 32481.980,
[],
0.025, 0.025,
18778.73, 18778.73,
), ),
( (
"ETH/USDT:USDT",
False, False,
"futures", "futures",
"cross", "cross",
1535443.01, 1535443.01,
71200.81144,
-56354.57,
135365.00, 135365.00,
3683.979, 3683.979, # amount
1456.84, 1456.84, # open_rate
[
{
# 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",
"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, 0.10,
1153.26, 1153.26,
), ),
( (
"BTC/USDT:USDT",
False, False,
"futures", "futures",
"cross", "cross",
1535443.01, 1535443.01,
356512.508, 16300.0,
-448192.89, 109.488, # amount
16300.000, 32481.980, # open_rate
109.488, [
32481.980, {
# 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",
"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, 0.025,
26316.89, 26316.89,
), ),
@@ -232,15 +275,15 @@ def test_stoploss_adjust_binance(mocker, default_conf, sl1, sl2, sl3, side):
def test_liquidation_price_binance( def test_liquidation_price_binance(
mocker, mocker,
default_conf, default_conf,
open_rate, pair,
is_short, is_short,
trading_mode, trading_mode,
margin_mode, margin_mode,
wallet_balance, wallet_balance,
mm_ex_1,
upnl_ex_1,
maintenance_amt, maintenance_amt,
amount, amount,
open_rate,
open_trades,
mm_ratio, mm_ratio,
expected, expected,
): ):
@@ -248,20 +291,48 @@ def test_liquidation_price_binance(
default_conf["margin_mode"] = margin_mode default_conf["margin_mode"] = margin_mode
default_conf["liquidation_buffer"] = 0.0 default_conf["liquidation_buffer"] = 0.0
exchange = get_patched_exchange(mocker, default_conf, exchange="binance") 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_ != 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
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
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 ( assert (
pytest.approx( pytest.approx(
round( round(
exchange.get_liquidation_price( exchange.get_liquidation_price(
pair="DOGE/USDT", pair=pair,
open_rate=open_rate, open_rate=open_rate,
is_short=is_short, is_short=is_short,
wallet_balance=wallet_balance, wallet_balance=wallet_balance,
mm_ex_1=mm_ex_1,
upnl_ex_1=upnl_ex_1,
amount=amount, amount=amount,
stake_amount=open_rate * amount, stake_amount=open_rate * amount,
leverage=5, leverage=5,
open_trades=open_trade_objects,
), ),
2, 2,
) )

View File

@@ -5524,8 +5524,6 @@ def test_liquidation_price_is_none(
stake_amount=open_rate * 71200.81144, stake_amount=open_rate * 71200.81144,
leverage=5, leverage=5,
wallet_balance=-56354.57, wallet_balance=-56354.57,
mm_ex_1=0.10,
upnl_ex_1=0.0,
) )
is None is None
) )
@@ -6011,6 +6009,7 @@ def test_get_liquidation_price1(mocker, default_conf):
stake_amount=18.884 * 0.8, stake_amount=18.884 * 0.8,
leverage=leverage, leverage=leverage,
wallet_balance=18.884 * 0.8, wallet_balance=18.884 * 0.8,
open_trades=[],
) )
@@ -6141,6 +6140,7 @@ def test_get_liquidation_price(
wallet_balance=amount * open_rate / leverage, wallet_balance=amount * open_rate / leverage,
leverage=leverage, leverage=leverage,
is_short=is_short, is_short=is_short,
open_trades=[],
) )
if expected_liq is None: if expected_liq is None:
assert liq is None assert liq is None

View File

@@ -457,6 +457,7 @@ class TestCCXTExchange:
stake_amount=100, stake_amount=100,
leverage=5, leverage=5,
wallet_balance=100, wallet_balance=100,
open_trades=[],
) )
assert isinstance(liquidation_price, float) assert isinstance(liquidation_price, float)
assert liquidation_price >= 0.0 assert liquidation_price >= 0.0
@@ -469,6 +470,7 @@ class TestCCXTExchange:
stake_amount=100, stake_amount=100,
leverage=5, leverage=5,
wallet_balance=100, wallet_balance=100,
open_trades=[],
) )
assert isinstance(liquidation_price, float) assert isinstance(liquidation_price, float)
assert liquidation_price >= 0.0 assert liquidation_price >= 0.0

View File

@@ -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