moved liquidation_price method to exchange classes

This commit is contained in:
Sam Germain 2022-01-22 20:03:38 -06:00
parent 5a97760bd1
commit 0c13e387fe
10 changed files with 358 additions and 527 deletions

View File

@ -2015,6 +2015,114 @@ class Exchange:
# TODO-lev: return the real amounts # TODO-lev: return the real amounts
return (0, 0.4) return (0, 0.4)
def liquidation_price(
self,
open_rate: float, # Entry price of position
is_short: bool,
leverage: float,
trading_mode: TradingMode,
mm_ratio: float,
collateral: Optional[Collateral] = Collateral.ISOLATED,
maintenance_amt: Optional[float] = None, # (Binance)
position: Optional[float] = None, # (Binance and Gateio) Absolute value of position size
wallet_balance: Optional[float] = None, # (Binance and Gateio)
taker_fee_rate: Optional[float] = None, # (Gateio & Okex)
liability: Optional[float] = None, # (Okex)
interest: Optional[float] = None, # (Okex)
position_assets: Optional[float] = None, # * (Okex) Might be same as position
mm_ex_1: Optional[float] = 0.0, # (Binance) Cross only
upnl_ex_1: Optional[float] = 0.0, # (Binance) Cross only
) -> Optional[float]:
"""
:param exchange_name:
:param open_rate: (EP1) Entry price of position
:param is_short: True if the trade is a short, false otherwise
:param leverage: The amount of leverage on the trade
:param trading_mode: SPOT, MARGIN, FUTURES, etc.
:param position: Absolute value of position size (in base currency)
:param mm_ratio: (MMR)
Okex: [assets in the position - (liability +interest) * mark price] /
(maintenance margin + liquidation fee)
# * Note: Binance's formula specifies maintenance margin rate which is mm_ratio * 100%
:param collateral: Either ISOLATED or CROSS
# * Binance
:param maintenance_amt: (CUM) Maintenance Amount of position
# * Binance and Gateio
:param wallet_balance: (WB)
Cross-Margin Mode: crossWalletBalance
Isolated-Margin Mode: isolatedWalletBalance
:param position: Absolute value of position size (in base currency)
# * Gateio & Okex
:param taker_fee_rate:
# * Okex
:param liability:
Initial liabilities + deducted interest
Long positions: Liability is calculated in quote currency.
Short positions: Liability is calculated in trading currency.
:param interest:
Interest that has not been deducted yet.
:param position_assets:
Total position assets on-hold by pending order
# * Cross only (Binance)
:param mm_ex_1: (TMM)
Cross-Margin Mode: Maintenance Margin of all other contracts, excluding Contract 1
Isolated-Margin Mode: 0
:param upnl_ex_1: (UPNL)
Cross-Margin Mode: Unrealized PNL of all other contracts, excluding Contract 1.
Isolated-Margin Mode: 0
"""
if trading_mode == TradingMode.SPOT:
return None
if not collateral:
raise OperationalException(
"Parameter collateral is required by liquidation_price when trading_mode is "
f"{trading_mode}"
)
return self.liquidation_price_helper(
open_rate,
is_short,
leverage,
trading_mode,
mm_ratio,
collateral,
maintenance_amt,
position,
wallet_balance,
taker_fee_rate,
liability,
interest,
position_assets,
mm_ex_1,
upnl_ex_1,
)
def liquidation_price_helper(
self,
open_rate: float,
is_short: bool,
leverage: float,
trading_mode: TradingMode,
mm_ratio: float,
collateral: Collateral,
maintenance_amt: Optional[float] = None,
position: Optional[float] = None,
wallet_balance: Optional[float] = None,
taker_fee_rate: Optional[float] = None,
liability: Optional[float] = None,
interest: Optional[float] = None,
position_assets: Optional[float] = None,
mm_ex_1: Optional[float] = 0.0,
upnl_ex_1: Optional[float] = 0.0,
) -> Optional[float]:
raise OperationalException(f"liquidation_price is not implemented for {self.name}")
def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = None) -> bool: def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = None) -> bool:
return exchange_name in ccxt_exchanges(ccxt_module) return exchange_name in ccxt_exchanges(ccxt_module)

View File

@ -51,3 +51,78 @@ class Gateio(Exchange):
""" """
info = self.markets[pair]['info'] info = self.markets[pair]['info']
return (float(info['maintenance_rate']), None) return (float(info['maintenance_rate']), None)
def liquidation_price_helper(
self,
open_rate: float, # Entry price of position
is_short: bool,
leverage: float,
trading_mode: TradingMode,
mm_ratio: float,
collateral: Collateral,
maintenance_amt: Optional[float] = None, # (Binance)
position: Optional[float] = None, # (Binance and Gateio) Absolute value of position size
wallet_balance: Optional[float] = None, # (Binance and Gateio)
taker_fee_rate: Optional[float] = None, # (Gateio & Okex)
liability: Optional[float] = None, # (Okex)
interest: Optional[float] = None, # (Okex)
position_assets: Optional[float] = None, # * (Okex) Might be same as position
mm_ex_1: Optional[float] = 0.0, # (Binance) Cross only
upnl_ex_1: Optional[float] = 0.0, # (Binance) Cross only
) -> Optional[float]:
"""
PERPETUAL: https://www.gate.io/help/futures/perpetual/22160/calculation-of-liquidation-price
:param exchange_name:
:param open_rate: Entry price of position
:param is_short: True if the trade is a short, false otherwise
:param leverage: The amount of leverage on the trade
:param trading_mode: SPOT, MARGIN, FUTURES, etc.
:param position: Absolute value of position size (in base currency)
:param mm_ratio:
:param collateral: Either ISOLATED or CROSS
:param maintenance_amt: # * Not required by Gateio
:param wallet_balance:
Cross-Margin Mode: crossWalletBalance
Isolated-Margin Mode: isolatedWalletBalance
:param position: Absolute value of position size (in base currency)
:param taker_fee_rate:
# * Not required by Gateio
:param liability:
:param interest:
:param position_assets:
:param mm_ex_1:
:param upnl_ex_1:
"""
if trading_mode == TradingMode.SPOT:
return None
if not collateral:
raise OperationalException(
"Parameter collateral is required by liquidation_price when trading_mode is "
f"{trading_mode}"
)
if (not wallet_balance or not position or not taker_fee_rate):
raise OperationalException(
"Parameters wallet_balance, position, taker_fee_rate"
"are required by Gateio.liquidation_price"
)
if trading_mode == TradingMode.FUTURES and collateral == Collateral.ISOLATED:
# if is_inverse:
# # ! Not implemented
# raise OperationalException(
# "Freqtrade does not support inverse contracts at the moment")
value = wallet_balance / position
mm_ratio_taker = (mm_ratio + taker_fee_rate)
if is_short:
return (open_rate + value) / (1 + mm_ratio_taker)
else:
return (open_rate - value) / (1 - mm_ratio_taker)
else:
raise OperationalException(
f"Gateio does not support {collateral.value} Mode {trading_mode.value} trading ")

View File

@ -1,7 +1,8 @@
import logging import logging
from typing import Dict, List, Tuple from typing import Dict, List, Optional, Tuple
from freqtrade.enums import Collateral, TradingMode from freqtrade.enums import Collateral, TradingMode
from freqtrade.exceptions import OperationalException
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
@ -26,3 +27,67 @@ class Okex(Exchange):
# (TradingMode.FUTURES, Collateral.CROSS), # (TradingMode.FUTURES, Collateral.CROSS),
# (TradingMode.FUTURES, Collateral.ISOLATED) # (TradingMode.FUTURES, Collateral.ISOLATED)
] ]
def liquidation_price_helper(
self,
open_rate: float, # Entry price of position
is_short: bool,
leverage: float,
trading_mode: TradingMode,
mm_ratio: float,
collateral: Collateral,
maintenance_amt: Optional[float] = None, # Not required
position: Optional[float] = None, # Not required
wallet_balance: Optional[float] = None, # Not required
taker_fee_rate: Optional[float] = None, # * required
liability: Optional[float] = None, # * required
interest: Optional[float] = None, # * required
position_assets: Optional[float] = None, # * required (Might be same as position)
mm_ex_1: Optional[float] = 0.0, # Not required
upnl_ex_1: Optional[float] = 0.0, # Not required
) -> Optional[float]:
"""
PERPETUAL: https://www.okex.com/support/hc/en-us/articles/
360053909592-VI-Introduction-to-the-isolated-mode-of-Single-Multi-currency-Portfolio-margin
:param exchange_name:
:param open_rate: (EP1) Entry price of position
:param is_short: True if the trade is a short, false otherwise
:param leverage: The amount of leverage on the trade
:param trading_mode: SPOT, MARGIN, FUTURES, etc.
:param position: Absolute value of position size (in base currency)
:param mm_ratio:
Okex: [assets in the position - (liability +interest) * mark price] /
(maintenance margin + liquidation fee)
:param collateral: Either ISOLATED or CROSS
:param maintenance_amt: # * Not required by Okex
:param wallet_balance: # * Not required by Okex
:param position: # * Not required by Okex
:param taker_fee_rate:
:param liability:
Initial liabilities + deducted interest
Long positions: Liability is calculated in quote currency.
Short positions: Liability is calculated in trading currency.
:param interest: Interest that has not been deducted yet.
:param position_assets: Total position assets on-hold by pending order
:param mm_ex_1: # * Not required by Okex
:param upnl_ex_1: # * Not required by Okex
"""
if (not liability or not interest or not taker_fee_rate or not position_assets):
raise OperationalException(
"Parameters liability, interest, taker_fee_rate, position_assets"
"are required by Okex.liquidation_price"
)
if trading_mode == TradingMode.FUTURES and collateral == Collateral.ISOLATED:
if is_short:
return (liability + interest) * (1 + mm_ratio) * (1 + taker_fee_rate)
else:
return (
(liability + interest) * (1 + mm_ratio) * (1 + taker_fee_rate) /
position_assets
)
else:
raise OperationalException(
f"Okex does not support {collateral.value} Mode {trading_mode.value} trading")

View File

@ -21,7 +21,6 @@ from freqtrade.enums import (Collateral, RPCMessageType, RunMode, SellType, Sign
from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError, from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError,
InvalidOrderException, PricingError) InvalidOrderException, PricingError)
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds
from freqtrade.leverage import liquidation_price
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, cleanup_db, init_db from freqtrade.persistence import Order, PairLocks, Trade, cleanup_db, init_db
@ -624,8 +623,7 @@ class FreqtradeBot(LoggingMixin):
amount amount
) )
taker_fee_rate = self.exchange.markets[pair]['taker'] taker_fee_rate = self.exchange.markets[pair]['taker']
isolated_liq = liquidation_price( isolated_liq = self.exchange.liquidation_price(
exchange_name=self.exchange.name,
open_rate=open_rate, open_rate=open_rate,
is_short=is_short, is_short=is_short,
leverage=leverage, leverage=leverage,

View File

@ -1,3 +1,2 @@
# flake8: noqa: F401 # flake8: noqa: F401
from freqtrade.leverage.interest import interest from freqtrade.leverage.interest import interest
from freqtrade.leverage.liquidation_price import liquidation_price

View File

@ -1,357 +0,0 @@
from typing import Optional
from freqtrade.enums import Collateral, TradingMode
from freqtrade.exceptions import OperationalException
def liquidation_price(
exchange_name: str,
open_rate: float, # Entry price of position
is_short: bool,
leverage: float,
trading_mode: TradingMode,
mm_ratio: float,
collateral: Optional[Collateral] = Collateral.ISOLATED,
maintenance_amt: Optional[float] = None, # (Binance)
position: Optional[float] = None, # (Binance and Gateio) Absolute value of position size
wallet_balance: Optional[float] = None, # (Binance and Gateio)
taker_fee_rate: Optional[float] = None, # (Gateio & Okex)
liability: Optional[float] = None, # (Okex)
interest: Optional[float] = None, # (Okex)
position_assets: Optional[float] = None, # * (Okex) Might be same as position
mm_ex_1: Optional[float] = 0.0, # (Binance) Cross only
upnl_ex_1: Optional[float] = 0.0, # (Binance) Cross only
) -> Optional[float]:
"""
:param exchange_name:
:param open_rate: (EP1) Entry price of position
:param is_short: True if the trade is a short, false otherwise
:param leverage: The amount of leverage on the trade
:param trading_mode: SPOT, MARGIN, FUTURES, etc.
:param position: Absolute value of position size (in base currency)
:param mm_ratio: (MMR)
Okex: [assets in the position - (liability +interest) * mark price] /
(maintenance margin + liquidation fee)
# * Note: Binance's formula specifies maintenance margin rate which is mm_ratio * 100%
:param collateral: Either ISOLATED or CROSS
# * Binance
:param maintenance_amt: (CUM) Maintenance Amount of position
# * Binance and Gateio
:param wallet_balance: (WB)
Cross-Margin Mode: crossWalletBalance
Isolated-Margin Mode: isolatedWalletBalance
:param position: Absolute value of position size (in base currency)
# * Gateio & Okex
:param taker_fee_rate:
# * Okex
:param liability:
Initial liabilities + deducted interest
Long positions: Liability is calculated in quote currency.
Short positions: Liability is calculated in trading currency.
:param interest:
Interest that has not been deducted yet.
:param position_assets:
Total position assets on-hold by pending order
# * Cross only (Binance)
:param mm_ex_1: (TMM)
Cross-Margin Mode: Maintenance Margin of all other contracts, excluding Contract 1
Isolated-Margin Mode: 0
:param upnl_ex_1: (UPNL)
Cross-Margin Mode: Unrealized PNL of all other contracts, excluding Contract 1.
Isolated-Margin Mode: 0
"""
if trading_mode == TradingMode.SPOT:
return None
if not collateral:
raise OperationalException(
"Parameter collateral is required by liquidation_price when trading_mode is "
f"{trading_mode}"
)
if exchange_name.lower() == "binance":
if (wallet_balance is None or maintenance_amt is None or position is None):
# mm_ex_1 is None or # * Cross only
# upnl_ex_1 is None or # * Cross only
raise OperationalException(
f"Parameters wallet_balance, maintenance_amt, position"
f"are required by liquidation_price when exchange is {exchange_name.lower()}"
)
# Suppress incompatible type "Optional[...]"; expected "..." as the check exists above.
return binance(
open_rate=open_rate,
is_short=is_short,
leverage=leverage,
trading_mode=trading_mode,
collateral=collateral, # type: ignore
wallet_balance=wallet_balance,
mm_ex_1=mm_ex_1, # type: ignore
upnl_ex_1=upnl_ex_1, # type: ignore
maintenance_amt=maintenance_amt, # type: ignore
position=position,
mm_ratio=mm_ratio,
)
elif exchange_name.lower() == "gateio":
if (not wallet_balance or not position or not taker_fee_rate):
raise OperationalException(
f"Parameters wallet_balance, position, taker_fee_rate"
f"are required by liquidation_price when exchange is {exchange_name.lower()}"
)
else:
return gateio(
open_rate=open_rate,
is_short=is_short,
trading_mode=trading_mode,
collateral=collateral,
wallet_balance=wallet_balance,
position=position,
mm_ratio=mm_ratio,
taker_fee_rate=taker_fee_rate
)
elif exchange_name.lower() == "okex":
if (not liability or not interest or not taker_fee_rate or not position_assets):
raise OperationalException(
f"Parameters liability, interest, taker_fee_rate, position_assets"
f"are required by liquidation_price when exchange is {exchange_name.lower()}"
)
else:
return okex(
is_short=is_short,
trading_mode=trading_mode,
collateral=collateral,
mm_ratio=mm_ratio,
liability=liability,
interest=interest,
taker_fee_rate=taker_fee_rate,
position_assets=position_assets,
)
elif exchange_name.lower() == "ftx":
return ftx(open_rate, is_short, leverage, trading_mode, collateral)
elif exchange_name.lower() == "kraken":
return kraken(open_rate, is_short, leverage, trading_mode, collateral)
raise OperationalException(f"liquidation_price is not implemented for {exchange_name}")
def exception(
exchange: str,
trading_mode: TradingMode,
collateral: Collateral,
):
"""
Raises an exception if exchange used doesn't support desired leverage mode
:param exchange: Name of the exchange
:param trading_mode: spot, margin, futures
:param collateral: cross, isolated
"""
raise OperationalException(
f"{exchange} does not support {collateral.value} Mode {trading_mode.value} trading ")
def binance(
open_rate: float,
is_short: bool,
leverage: float,
trading_mode: TradingMode,
mm_ratio: float,
collateral: Collateral,
maintenance_amt: float,
wallet_balance: float,
position: float,
mm_ex_1: float,
upnl_ex_1: float,
):
"""
MARGIN: https://www.binance.com/en/support/faq/f6b010588e55413aa58b7d63ee0125ed
PERPETUAL: https://www.binance.com/en/support/faq/b3c689c1f50a44cabb3a84e663b81d93
:param open_rate: (EP1) Entry Price of position (one-way mode)
:param is_short: true or false
:param leverage: leverage in float
:param trading_mode: SPOT, MARGIN, FUTURES
:param mm_ratio: (MMR) Maintenance margin rate of position (one-way mode)
:param collateral: CROSS, ISOLATED
:param maintenance_amt: (CUM) Maintenance Amount of position (one-way mode)
:param position: Absolute value of position size (one-way mode)
:param wallet_balance: (WB)
Cross-Margin Mode: crossWalletBalance
Isolated-Margin Mode: isolatedWalletBalance
TMM=0, UPNL=0, substitute the position quantity, MMR, cum into the formula to calculate.
Under the cross margin mode, the same ticker/symbol,
both long and short position share the same liquidation price except in isolated mode.
Under the isolated mode, each isolated position will have different liquidation prices
depending on the margin allocated to the positions.
:param mm_ex_1: (TMM)
Cross-Margin Mode: Maintenance Margin of all other contracts, excluding Contract 1
Isolated-Margin Mode: 0
:param upnl_ex_1: (UPNL)
Cross-Margin Mode: Unrealized PNL of all other contracts, excluding Contract 1.
Isolated-Margin Mode: 0
"""
side_1 = -1 if is_short else 1
position = abs(position)
cross_vars = upnl_ex_1 - mm_ex_1 if collateral == Collateral.CROSS else 0.0
if trading_mode == TradingMode.MARGIN and collateral == Collateral.CROSS:
# ! Not Implemented
exception("binance", trading_mode, collateral)
if trading_mode == TradingMode.FUTURES:
return (wallet_balance + cross_vars + maintenance_amt - (side_1 * position * open_rate)) / (
(position * mm_ratio) - (side_1 * position))
exception("binance", trading_mode, collateral)
def gateio(
open_rate: float,
is_short: bool,
trading_mode: TradingMode,
mm_ratio: float,
collateral: Collateral,
position: float,
wallet_balance: float,
taker_fee_rate: float,
is_inverse: bool = False
):
"""
PERPETUAL: https://www.gate.io/help/futures/perpetual/22160/calculation-of-liquidation-price
:param open_rate: Entry Price of position
:param is_short: True for short trades
:param trading_mode: SPOT, MARGIN, FUTURES
:param mm_ratio: Viewed in contract details
:param collateral: CROSS, ISOLATED
:param position: size of position in base currency
contract_size / num_contracts
contract_size: How much one contract is worth
num_contracts: Also called position
:param wallet_balance: Also called margin
:param taker_fee_rate:
:param is_inverse: True if settle currency matches base currency
( Opening Price ± Margin/Contract Multiplier/Position ) / [ 1 ± ( MMR + Taker Fee)]
'±' in the formula refers to the direction of the contract,
go long refers to '-'
go short refers to '+'
"""
if trading_mode == TradingMode.FUTURES and collateral == Collateral.ISOLATED:
if is_inverse:
# ! Not implemented
raise OperationalException("Freqtrade does not support inverse contracts at the moment")
value = wallet_balance / position
mm_ratio_taker = (mm_ratio + taker_fee_rate)
if is_short:
return (open_rate + value) / (1 + mm_ratio_taker)
else:
return (open_rate - value) / (1 - mm_ratio_taker)
else:
exception("gatio", trading_mode, collateral)
def okex(
is_short: bool,
trading_mode: TradingMode,
mm_ratio: float,
collateral: Collateral,
taker_fee_rate: float,
liability: float,
interest: float,
position_assets: float
):
"""
PERPETUAL: https://www.okex.com/support/hc/en-us/articles/
360053909592-VI-Introduction-to-the-isolated-mode-of-Single-Multi-currency-Portfolio-margin
:param is_short: True if the position is short, false otherwise
:param trading_mode: SPOT, MARGIN, FUTURES
:param mm_ratio:
long: [position_assets - (liability + interest) / mark_price] / (maintenance_margin + fees)
short: [position_assets - (liability + interest) * mark_price] / (maintenance_margin + fees)
:param collateral: CROSS, ISOLATED
:param taker_fee_rate:
:param liability: Initial liabilities + deducted interest
long: Liability is calculated in quote currency
short: Liability is calculated in trading currency
:param interest: Interest that has not been deducted yet
:param position_assets: Total position assets - on-hold by pending order
Total: The number of positive assets on the position (including margin).
long: with trading currency as position asset.
short: with quote currency as position asset.
Est. liquidation price
long: (liability + interest* (1 + maintenance margin ratio) *
(1 + taker fee rate) / position assets
short: (liability + interest* (1 + maintenance margin ratio) *
(1 + taker fee rate)
"""
if trading_mode == TradingMode.FUTURES and collateral == Collateral.ISOLATED:
if is_short:
return (liability + interest) * (1 + mm_ratio) * (1 + taker_fee_rate)
else:
return (liability + interest) * (1 + mm_ratio) * (1 + taker_fee_rate) / position_assets
else:
exception("okex", trading_mode, collateral)
def ftx(
open_rate: float,
is_short: bool,
leverage: float,
trading_mode: TradingMode,
collateral: Collateral
# ...
):
"""
# ! Not Implemented
Calculates the liquidation price on FTX
:param open_rate: Entry price of position
:param is_short: True if the trade is a short, false otherwise
:param leverage: The amount of leverage on the trade
:param trading_mode: SPOT, MARGIN, FUTURES, etc.
:param collateral: Either ISOLATED or CROSS
"""
if collateral == Collateral.CROSS:
exception("ftx", trading_mode, collateral)
# If nothing was returned
exception("ftx", trading_mode, collateral)
def kraken(
open_rate: float,
is_short: bool,
leverage: float,
trading_mode: TradingMode,
collateral: Collateral
# ...
):
"""
# ! Not Implemented
MARGIN:
https://support.kraken.com/hc/en-us/articles/203325763-Margin-Call-Level-and-Margin-Liquidation-Level
:param open_rate: Entry price of position
:param is_short: True if the trade is a short, false otherwise
:param leverage: The amount of leverage on the trade
:param trading_mode: SPOT, MARGIN, FUTURES, etc.
:param collateral: Either ISOLATED or CROSS
"""
if collateral == Collateral.CROSS:
if trading_mode == TradingMode.MARGIN:
exception("kraken", trading_mode, collateral)
elif trading_mode == TradingMode.FUTURES:
exception("kraken", trading_mode, collateral)
# If nothing was returned
exception("kraken", trading_mode, collateral)

View File

@ -14,9 +14,9 @@ from sqlalchemy.pool import StaticPool
from sqlalchemy.sql.schema import UniqueConstraint from sqlalchemy.sql.schema import UniqueConstraint
from freqtrade.constants import DATETIME_PRINT_FORMAT, NON_OPEN_EXCHANGE_STATES from freqtrade.constants import DATETIME_PRINT_FORMAT, NON_OPEN_EXCHANGE_STATES
from freqtrade.enums import Collateral, SellType, TradingMode from freqtrade.enums import SellType, TradingMode
from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.exceptions import DependencyException, OperationalException
from freqtrade.leverage import interest, liquidation_price from freqtrade.leverage import interest
from freqtrade.misc import safe_value_fallback from freqtrade.misc import safe_value_fallback
from freqtrade.persistence.migrations import check_migrate from freqtrade.persistence.migrations import check_migrate
@ -364,52 +364,12 @@ class LocalTrade():
def set_isolated_liq( def set_isolated_liq(
self, self,
isolated_liq: Optional[float] = None, isolated_liq: float,
wallet_balance: Optional[float] = None,
current_price: Optional[float] = None,
maintenance_amt: Optional[float] = None,
mm_ratio: Optional[float] = None,
): ):
""" """
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 isolated_liq:
if not wallet_balance or not current_price:
raise OperationalException(
"wallet balance must be passed to LocalTrade.set_isolated_liq when param"
"isolated_liq is None"
)
if (
mm_ratio is None or
wallet_balance is None or
current_price is None or
maintenance_amt is None
):
raise OperationalException(
'mm_ratio, wallet_balance, current_price and maintenance_amt '
'required in set_isolated_liq when isolated_liq is None'
)
isolated_liq = liquidation_price(
exchange_name=self.exchange,
open_rate=self.open_rate,
is_short=self.is_short,
leverage=self.leverage,
trading_mode=self.trading_mode,
collateral=Collateral.ISOLATED,
mm_ex_1=0.0,
upnl_ex_1=0.0,
position=self.amount * current_price,
wallet_balance=self.amount / self.leverage, # TODO: Update for cross
maintenance_amt=maintenance_amt,
mm_ratio=mm_ratio,
)
if isolated_liq is None:
raise OperationalException(
"leverage/isolated_liq returned None. This exception should never happen"
)
if self.stop_loss is not None: if self.stop_loss is not None:
if self.is_short: if self.is_short:
self.stop_loss = min(self.stop_loss, isolated_liq) self.stop_loss = min(self.stop_loss, isolated_liq)

View File

@ -26,6 +26,12 @@ from tests.conftest import get_mock_coro, get_patched_exchange, log_has, log_has
# Make sure to always keep one exchange here which is NOT subclassed!! # Make sure to always keep one exchange here which is NOT subclassed!!
EXCHANGES = ['bittrex', 'binance', 'kraken', 'ftx', 'gateio'] EXCHANGES = ['bittrex', 'binance', 'kraken', 'ftx', 'gateio']
spot = TradingMode.SPOT
margin = TradingMode.MARGIN
futures = TradingMode.FUTURES
cross = Collateral.CROSS
isolated = Collateral.ISOLATED
def ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, def ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name,
@ -3965,3 +3971,102 @@ def test__amount_to_contracts(
assert result_size == param_size assert result_size == param_size
result_amount = exchange._contracts_to_amount(pair, param_size) result_amount = exchange._contracts_to_amount(pair, param_size)
assert result_amount == param_amount assert result_amount == param_amount
@pytest.mark.parametrize('exchange_name,open_rate,is_short,leverage,trading_mode,collateral', [
# Bittrex
('bittrex', "2.0", False, "3.0", spot, None),
('bittrex', "2.0", False, "1.0", spot, cross),
('bittrex', "2.0", True, "3.0", spot, isolated),
# Binance
('binance', "2.0", False, "3.0", spot, None),
('binance', "2.0", False, "1.0", spot, cross),
('binance', "2.0", True, "3.0", spot, isolated),
])
def test_liquidation_price_is_none(
mocker,
default_conf,
exchange_name,
open_rate,
is_short,
leverage,
trading_mode,
collateral
):
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
assert exchange.liquidation_price(
open_rate,
is_short,
leverage,
trading_mode,
collateral,
1535443.01,
71200.81144,
-56354.57,
135365.00,
3683.979,
0.10,
) is None
@pytest.mark.parametrize('exchange_name,open_rate,is_short,leverage,trading_mode,collateral', [
# Bittrex
('bittrex', "2.0", False, "3.0", margin, cross),
('bittrex', "2.0", False, "3.0", margin, isolated),
('bittrex', "2.0", False, "3.0", futures, cross),
('bittrex', "2.0", False, "3.0", futures, isolated),
# Binance
# Binance supports isolated margin, but freqtrade likely won't for a while on Binance
('binance', "2.0", True, "3.0", margin, isolated),
# Kraken
('kraken', "2.0", True, "1.0", margin, isolated),
('kraken', "2.0", True, "1.0", futures, isolated),
# FTX
('ftx', "2.0", True, "3.0", margin, isolated),
('ftx', "2.0", True, "3.0", futures, isolated),
])
def test_liquidation_price_exception_thrown(
exchange_name,
open_rate,
is_short,
leverage,
trading_mode,
collateral,
result
):
# TODO-lev assert exception is thrown
return # Here to avoid indent error, remove when implemented
@pytest.mark.parametrize(
'exchange_name, is_short, leverage, trading_mode, collateral, wallet_balance, '
'mm_ex_1, upnl_ex_1, maintenance_amt, position, open_rate, '
'mm_ratio, expected',
[
("binance", False, 1, futures, isolated, 1535443.01, 0.0,
0.0, 135365.00, 3683.979, 1456.84, 0.10, 1114.78),
("binance", False, 1, futures, isolated, 1535443.01, 0.0,
0.0, 16300.000, 109.488, 32481.980, 0.025, 18778.73),
("binance", False, 1, futures, cross, 1535443.01, 71200.81144,
-56354.57, 135365.00, 3683.979, 1456.84, 0.10, 1153.26),
("binance", False, 1, futures, cross, 1535443.01, 356512.508,
-448192.89, 16300.000, 109.488, 32481.980, 0.025, 26316.89)
])
def test_liquidation_price(
mocker, default_conf, exchange_name, open_rate, is_short, leverage, trading_mode,
collateral, wallet_balance, mm_ex_1, upnl_ex_1, maintenance_amt, position, mm_ratio, expected
):
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
assert isclose(round(exchange.liquidation_price(
open_rate=open_rate,
is_short=is_short,
leverage=leverage,
trading_mode=trading_mode,
collateral=collateral,
wallet_balance=wallet_balance,
mm_ex_1=mm_ex_1,
upnl_ex_1=upnl_ex_1,
maintenance_amt=maintenance_amt,
position=position,
mm_ratio=mm_ratio
), 2), expected)

View File

@ -1,121 +0,0 @@
from math import isclose
import pytest
from freqtrade.enums import Collateral, TradingMode
from freqtrade.leverage import liquidation_price
# from freqtrade.exceptions import OperationalException
spot = TradingMode.SPOT
margin = TradingMode.MARGIN
futures = TradingMode.FUTURES
cross = Collateral.CROSS
isolated = Collateral.ISOLATED
@pytest.mark.parametrize('exchange_name,open_rate,is_short,leverage,trading_mode,collateral', [
# Bittrex
('bittrex', "2.0", False, "3.0", spot, None),
('bittrex', "2.0", False, "1.0", spot, cross),
('bittrex', "2.0", True, "3.0", spot, isolated),
# Binance
('binance', "2.0", False, "3.0", spot, None),
('binance', "2.0", False, "1.0", spot, cross),
('binance', "2.0", True, "3.0", spot, isolated),
# Kraken
('kraken', "2.0", False, "3.0", spot, None),
('kraken', "2.0", True, "3.0", spot, cross),
('kraken', "2.0", False, "1.0", spot, isolated),
# FTX
('ftx', "2.0", True, "3.0", spot, None),
('ftx', "2.0", False, "3.0", spot, cross),
('ftx', "2.0", False, "3.0", spot, isolated),
])
def test_liquidation_price_is_none(
exchange_name,
open_rate,
is_short,
leverage,
trading_mode,
collateral
):
assert liquidation_price(
exchange_name,
open_rate,
is_short,
leverage,
trading_mode,
collateral,
1535443.01,
71200.81144,
-56354.57,
135365.00,
3683.979,
0.10,
) is None
@pytest.mark.parametrize('exchange_name,open_rate,is_short,leverage,trading_mode,collateral', [
# Bittrex
('bittrex', "2.0", False, "3.0", margin, cross),
('bittrex', "2.0", False, "3.0", margin, isolated),
('bittrex', "2.0", False, "3.0", futures, cross),
('bittrex', "2.0", False, "3.0", futures, isolated),
# Binance
# Binance supports isolated margin, but freqtrade likely won't for a while on Binance
('binance', "2.0", True, "3.0", margin, isolated),
# Kraken
('kraken', "2.0", False, "1.0", margin, isolated),
('kraken', "2.0", False, "1.0", futures, isolated),
# FTX
('ftx', "2.0", False, "3.0", margin, isolated),
('ftx', "2.0", False, "3.0", futures, isolated),
])
def test_liquidation_price_exception_thrown(
exchange_name,
open_rate,
is_short,
leverage,
trading_mode,
collateral,
result
):
# TODO-lev assert exception is thrown
return # Here to avoid indent error, remove when implemented
@pytest.mark.parametrize(
'exchange_name, is_short, leverage, trading_mode, collateral, wallet_balance, '
'mm_ex_1, upnl_ex_1, maintenance_amt, position, open_rate, '
'mm_ratio, expected',
[
("binance", False, 1, TradingMode.FUTURES, Collateral.ISOLATED, 1535443.01, 0.0,
0.0, 135365.00, 3683.979, 1456.84, 0.10, 1114.78),
("binance", False, 1, TradingMode.FUTURES, Collateral.ISOLATED, 1535443.01, 0.0,
0.0, 16300.000, 109.488, 32481.980, 0.025, 18778.73),
("binance", False, 1, TradingMode.FUTURES, Collateral.CROSS, 1535443.01, 71200.81144,
-56354.57, 135365.00, 3683.979, 1456.84, 0.10, 1153.26),
("binance", False, 1, TradingMode.FUTURES, Collateral.CROSS, 1535443.01, 356512.508,
-448192.89, 16300.000, 109.488, 32481.980, 0.025, 26316.89)
])
def test_liquidation_price(
exchange_name, open_rate, is_short, leverage, trading_mode, collateral, wallet_balance,
mm_ex_1, upnl_ex_1, maintenance_amt, position, mm_ratio, expected
):
assert isclose(round(liquidation_price(
exchange_name=exchange_name,
open_rate=open_rate,
is_short=is_short,
leverage=leverage,
trading_mode=trading_mode,
collateral=collateral,
wallet_balance=wallet_balance,
mm_ex_1=mm_ex_1,
upnl_ex_1=upnl_ex_1,
maintenance_amt=maintenance_amt,
position=position,
mm_ratio=mm_ratio
), 2), expected)

View File

@ -734,7 +734,6 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order,
((wb + cum_b) - (side_1 * position * ep1)) / ((position * mmr_b) - (side_1 * position)) ((wb + cum_b) - (side_1 * position * ep1)) / ((position * mmr_b) - (side_1 * position))
((2 + 0.01) - (1 * 1 * 10)) / ((1 * 0.01) - (1 * 1)) = 8.070707070707071 ((2 + 0.01) - (1 * 1 * 10)) / ((1 * 0.01) - (1 * 1)) = 8.070707070707071
exchange_name = gateio, is_short = true exchange_name = gateio, is_short = true
(open_rate + (wallet_balance / position)) / (1 + (mm_ratio + taker_fee_rate)) (open_rate + (wallet_balance / position)) / (1 + (mm_ratio + taker_fee_rate))
(10 + (2 / 1)) / (1 + (0.01 + 0.0006)) = 11.87413417771621 (10 + (2 / 1)) / (1 + (0.01 + 0.0006)) = 11.87413417771621