mirror of
https://github.com/freqtrade/freqtrade.git
synced 2025-11-29 08:33:07 +00:00
Merge pull request #10741 from freqtrade/feat/improve_liquidation_logic
Improved liquidation price logic
This commit is contained in:
@@ -144,6 +144,29 @@ 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, 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,
|
||||
@@ -153,8 +176,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
|
||||
open_trades: list,
|
||||
) -> Optional[float]:
|
||||
"""
|
||||
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
|
||||
Cross-Margin Mode: crossWalletBalance
|
||||
Isolated-Margin Mode: isolatedWalletBalance
|
||||
:param open_trades: List of open trades in the same wallet
|
||||
|
||||
# * Only required for Cross
|
||||
:param mm_ex_1: (TMM)
|
||||
@@ -180,15 +203,41 @@ 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
|
||||
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:
|
||||
raise OperationalException(
|
||||
"Parameter maintenance_amt is required by Binance.liquidation_price"
|
||||
|
||||
@@ -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
|
||||
open_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 open_trades: List of other open trades in the same wallet
|
||||
"""
|
||||
|
||||
market = self.markets[pair]
|
||||
|
||||
@@ -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
|
||||
open_trades: Optional[list] = None,
|
||||
) -> 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,
|
||||
open_trades=open_trades or [],
|
||||
)
|
||||
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
|
||||
open_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 open_trades: List of other open trades in the same wallet
|
||||
"""
|
||||
|
||||
market = self.markets[pair]
|
||||
|
||||
@@ -43,6 +43,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
|
||||
@@ -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()
|
||||
@@ -2233,20 +2245,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:
|
||||
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,
|
||||
)
|
||||
)
|
||||
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
|
||||
|
||||
66
freqtrade/leverage/liquidation_price.py
Normal file
66
freqtrade/leverage/liquidation_price.py
Normal 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")
|
||||
@@ -26,6 +26,7 @@ from freqtrade.enums import (
|
||||
CandleType,
|
||||
ExitCheckTuple,
|
||||
ExitType,
|
||||
MarginMode,
|
||||
RunMode,
|
||||
TradingMode,
|
||||
)
|
||||
@@ -37,6 +38,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
|
||||
@@ -206,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)
|
||||
@@ -698,21 +701,20 @@ 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
|
||||
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,
|
||||
)
|
||||
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,
|
||||
wallets=self.wallets,
|
||||
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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import pytest
|
||||
|
||||
from freqtrade.enums import CandleType, MarginMode, TradingMode
|
||||
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.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(
|
||||
"is_short, trading_mode, margin_mode, wallet_balance, "
|
||||
"mm_ex_1, upnl_ex_1, maintenance_amt, amount, open_rate, "
|
||||
"pair, is_short, trading_mode, margin_mode, wallet_balance, "
|
||||
"maintenance_amt, amount, open_rate, open_trades,"
|
||||
"mm_ratio, expected",
|
||||
[
|
||||
(
|
||||
"ETH/USDT:USDT",
|
||||
False,
|
||||
"futures",
|
||||
"isolated",
|
||||
1535443.01,
|
||||
0.0,
|
||||
0.0,
|
||||
135365.00,
|
||||
3683.979,
|
||||
1456.84,
|
||||
[],
|
||||
0.10,
|
||||
1114.78,
|
||||
),
|
||||
(
|
||||
"ETH/USDT:USDT",
|
||||
False,
|
||||
"futures",
|
||||
"isolated",
|
||||
1535443.01,
|
||||
0.0,
|
||||
0.0,
|
||||
16300.000,
|
||||
109.488,
|
||||
32481.980,
|
||||
[],
|
||||
0.025,
|
||||
18778.73,
|
||||
),
|
||||
(
|
||||
"ETH/USDT:USDT",
|
||||
False,
|
||||
"futures",
|
||||
"cross",
|
||||
1535443.01,
|
||||
71200.81144,
|
||||
-56354.57,
|
||||
135365.00,
|
||||
3683.979,
|
||||
1456.84,
|
||||
3683.979, # amount
|
||||
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,
|
||||
1153.26,
|
||||
),
|
||||
(
|
||||
"BTC/USDT:USDT",
|
||||
False,
|
||||
"futures",
|
||||
"cross",
|
||||
1535443.01,
|
||||
356512.508,
|
||||
-448192.89,
|
||||
16300.000,
|
||||
109.488,
|
||||
32481.980,
|
||||
16300.0,
|
||||
109.488, # amount
|
||||
32481.980, # 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.025,
|
||||
26316.89,
|
||||
),
|
||||
@@ -232,15 +275,15 @@ def test_stoploss_adjust_binance(mocker, default_conf, sl1, sl2, sl3, side):
|
||||
def test_liquidation_price_binance(
|
||||
mocker,
|
||||
default_conf,
|
||||
open_rate,
|
||||
pair,
|
||||
is_short,
|
||||
trading_mode,
|
||||
margin_mode,
|
||||
wallet_balance,
|
||||
mm_ex_1,
|
||||
upnl_ex_1,
|
||||
maintenance_amt,
|
||||
amount,
|
||||
open_rate,
|
||||
open_trades,
|
||||
mm_ratio,
|
||||
expected,
|
||||
):
|
||||
@@ -248,20 +291,48 @@ 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_ != 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 (
|
||||
pytest.approx(
|
||||
round(
|
||||
exchange.get_liquidation_price(
|
||||
pair="DOGE/USDT",
|
||||
pair=pair,
|
||||
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,
|
||||
open_trades=open_trade_objects,
|
||||
),
|
||||
2,
|
||||
)
|
||||
|
||||
@@ -5524,8 +5524,6 @@ 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,
|
||||
)
|
||||
is None
|
||||
)
|
||||
@@ -6011,6 +6009,7 @@ def test_get_liquidation_price1(mocker, default_conf):
|
||||
stake_amount=18.884 * 0.8,
|
||||
leverage=leverage,
|
||||
wallet_balance=18.884 * 0.8,
|
||||
open_trades=[],
|
||||
)
|
||||
|
||||
|
||||
@@ -6141,6 +6140,7 @@ def test_get_liquidation_price(
|
||||
wallet_balance=amount * open_rate / leverage,
|
||||
leverage=leverage,
|
||||
is_short=is_short,
|
||||
open_trades=[],
|
||||
)
|
||||
if expected_liq is None:
|
||||
assert liq is None
|
||||
|
||||
@@ -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
|
||||
|
||||
57
tests/leverage/test_update_liquidation_price.py
Normal file
57
tests/leverage/test_update_liquidation_price.py
Normal 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
|
||||
Reference in New Issue
Block a user