Merge pull request #5849 from freqtrade/isolated-liq

Isolated liq
This commit is contained in:
Matthias 2022-02-01 06:36:46 +01:00 committed by GitHub
commit 2141e04a19
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 728 additions and 204 deletions

View File

@ -155,7 +155,7 @@ CONF_SCHEMA = {
'ignore_roi_if_buy_signal': {'type': 'boolean'}, 'ignore_roi_if_buy_signal': {'type': 'boolean'},
'ignore_buying_expired_candle_after': {'type': 'number'}, 'ignore_buying_expired_candle_after': {'type': 'number'},
'trading_mode': {'type': 'string', 'enum': TRADING_MODES}, 'trading_mode': {'type': 'string', 'enum': TRADING_MODES},
'collateral_type': {'type': 'string', 'enum': COLLATERAL_TYPES}, 'collateral': {'type': 'string', 'enum': COLLATERAL_TYPES},
'backtest_breakdown': { 'backtest_breakdown': {
'type': 'array', 'type': 'array',
'items': {'type': 'string', 'enum': BACKTEST_BREAKDOWNS} 'items': {'type': 'string', 'enum': BACKTEST_BREAKDOWNS}

View File

@ -33,10 +33,9 @@ class Binance(Exchange):
_supported_trading_mode_collateral_pairs: List[Tuple[TradingMode, Collateral]] = [ _supported_trading_mode_collateral_pairs: List[Tuple[TradingMode, Collateral]] = [
# TradingMode.SPOT always supported and not required in this list # TradingMode.SPOT always supported and not required in this list
# TODO-lev: Uncomment once supported
# (TradingMode.MARGIN, Collateral.CROSS), # (TradingMode.MARGIN, Collateral.CROSS),
# (TradingMode.FUTURES, Collateral.CROSS), # (TradingMode.FUTURES, Collateral.CROSS),
# (TradingMode.FUTURES, Collateral.ISOLATED) (TradingMode.FUTURES, Collateral.ISOLATED)
] ]
def stoploss_adjust(self, stop_loss: float, order: Dict, side: str) -> bool: def stoploss_adjust(self, stop_loss: float, order: Dict, side: str) -> bool:
@ -120,10 +119,25 @@ class Binance(Exchange):
raise OperationalException(e) from e raise OperationalException(e) from e
@retrier @retrier
def fill_leverage_brackets(self): def fill_leverage_brackets(self) -> None:
""" """
Assigns property _leverage_brackets to a dictionary of information about the leverage Assigns property _leverage_brackets to a dictionary of information about the leverage
allowed on each pair allowed on each pair
After exectution, self._leverage_brackets = {
"pair_name": [
[notional_floor, maintenenace_margin_ratio, maintenance_amt],
...
],
...
}
e.g. {
"ETH/USDT:USDT": [
[0.0, 0.01, 0.0],
[10000, 0.02, 0.01],
...
],
...
}
""" """
if self.trading_mode == TradingMode.FUTURES: if self.trading_mode == TradingMode.FUTURES:
try: try:
@ -136,17 +150,21 @@ class Binance(Exchange):
else: else:
leverage_brackets = self._api.load_leverage_brackets() leverage_brackets = self._api.load_leverage_brackets()
for pair, brackets in leverage_brackets.items(): for pair, brkts in leverage_brackets.items():
self._leverage_brackets[pair] = [ [amt, old_ratio] = [0.0, 0.0]
[ brackets = []
min_amount, for [notional_floor, mm_ratio] in brkts:
float(margin_req) amt = (
] for [ (float(notional_floor) * (float(mm_ratio) - float(old_ratio)))
min_amount, + amt
margin_req ) if old_ratio else 0.0
] in brackets old_ratio = mm_ratio
] brackets.append([
float(notional_floor),
float(mm_ratio),
amt,
])
self._leverage_brackets[pair] = brackets
except ccxt.DDoSProtection as e: except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e raise DDosProtection(e) from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e: except (ccxt.NetworkError, ccxt.ExchangeError) as e:
@ -161,16 +179,22 @@ class Binance(Exchange):
:param pair: The base/quote currency pair being traded :param pair: The base/quote currency pair being traded
:stake_amount: The total value of the traders collateral in quote currency :stake_amount: The total value of the traders collateral in quote currency
""" """
if stake_amount is None:
raise OperationalException('binance.get_max_leverage requires argument stake_amount')
if pair not in self._leverage_brackets: if pair not in self._leverage_brackets:
return 1.0 return 1.0
pair_brackets = self._leverage_brackets[pair] pair_brackets = self._leverage_brackets[pair]
num_brackets = len(pair_brackets) num_brackets = len(pair_brackets)
min_amount = 0 min_amount = 0.0
for bracket_num in range(num_brackets): for bracket_num in range(num_brackets):
[_, margin_req] = pair_brackets[bracket_num] [notional_floor, mm_ratio, _] = pair_brackets[bracket_num]
lev = 1/margin_req lev = 1.0
if mm_ratio != 0:
lev = 1.0/mm_ratio
else:
logger.warning(f"mm_ratio for {pair} with notional floor {notional_floor} is 0")
if bracket_num+1 != num_brackets: # If not on last bracket if bracket_num+1 != num_brackets: # If not on last bracket
[min_amount, _] = pair_brackets[bracket_num+1] # Get min_amount of next bracket [min_amount, _, __] = pair_brackets[bracket_num+1] # Get min_amount of next bracket
else: else:
return lev return lev
nominal_value = stake_amount * lev nominal_value = stake_amount * lev
@ -237,3 +261,90 @@ class Binance(Exchange):
:return: The cutoff open time for when a funding fee is charged :return: The cutoff open time for when a funding fee is charged
""" """
return open_date.minute > 0 or (open_date.minute == 0 and open_date.second > 15) return open_date.minute > 0 or (open_date.minute == 0 and open_date.second > 15)
def get_maintenance_ratio_and_amt(
self,
pair: str,
nominal_value: Optional[float] = 0.0,
) -> Tuple[float, Optional[float]]:
"""
Formula: https://www.binance.com/en/support/faq/b3c689c1f50a44cabb3a84e663b81d93
Maintenance amt = Floor of Position Bracket on Level n *
difference between
Maintenance Margin Rate on Level n and
Maintenance Margin Rate on Level n-1)
+ Maintenance Amount on Level n-1
:return: The maintenance margin ratio and maintenance amount
"""
if nominal_value is None:
raise OperationalException(
"nominal value is required for binance.get_maintenance_ratio_and_amt")
if pair not in self._leverage_brackets:
raise InvalidOrderException(f"Cannot calculate liquidation price for {pair}")
pair_brackets = self._leverage_brackets[pair]
for [notional_floor, mm_ratio, amt] in reversed(pair_brackets):
if nominal_value >= notional_floor:
return (mm_ratio, amt)
raise OperationalException("nominal value can not be lower than 0")
# The lowest notional_floor for any pair in loadLeverageBrackets is always 0 because it
# describes the min amount for a bracket, and the lowest bracket will always go down to 0
def dry_run_liquidation_price(
self,
pair: str,
open_rate: float, # Entry price of position
is_short: bool,
position: float, # Absolute value of position size
wallet_balance: float, # Or margin balance
mm_ex_1: float = 0.0, # (Binance) Cross only
upnl_ex_1: float = 0.0, # (Binance) Cross only
) -> Optional[float]:
"""
MARGIN: https://www.binance.com/en/support/faq/f6b010588e55413aa58b7d63ee0125ed
PERPETUAL: https://www.binance.com/en/support/faq/b3c689c1f50a44cabb3a84e663b81d93
:param exchange_name:
:param open_rate: (EP1) Entry price of position
:param is_short: True if the trade is a short, false otherwise
:param position: Absolute value of position size (in base currency)
:param wallet_balance: (WB)
Cross-Margin Mode: crossWalletBalance
Isolated-Margin Mode: isolatedWalletBalance
:param maintenance_amt:
# * Only required for Cross
: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 self.collateral == Collateral.CROSS else 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, position)
if (maintenance_amt is None):
raise OperationalException(
"Parameter maintenance_amt is required by Binance.liquidation_price"
f"for {self.trading_mode.value}"
)
if self.trading_mode == TradingMode.FUTURES:
return (
(
(wallet_balance + cross_vars + maintenance_amt) -
(side_1 * position * open_rate)
) / (
(position * mm_ratio) - (side_1 * position)
)
)
else:
raise OperationalException(
"Freqtrade only supports isolated futures for leverage trading")

View File

@ -90,7 +90,7 @@ class Exchange:
self._api: ccxt.Exchange = None self._api: ccxt.Exchange = None
self._api_async: ccxt_async.Exchange = None self._api_async: ccxt_async.Exchange = None
self._markets: Dict = {} self._markets: Dict = {}
self._leverage_brackets: Dict = {} self._leverage_brackets: Dict[str, List[List[float]]] = {}
self.loop = asyncio.new_event_loop() self.loop = asyncio.new_event_loop()
asyncio.set_event_loop(self.loop) asyncio.set_event_loop(self.loop)
@ -633,7 +633,6 @@ class Exchange:
Re-implementation of ccxt internal methods - ensuring we can test the result is correct Re-implementation of ccxt internal methods - ensuring we can test the result is correct
based on our definitions. based on our definitions.
""" """
amount = self._amount_to_contracts(pair, amount)
if self.markets[pair]['precision']['amount']: if self.markets[pair]['precision']['amount']:
amount = float(decimal_to_precision(amount, rounding_mode=TRUNCATE, amount = float(decimal_to_precision(amount, rounding_mode=TRUNCATE,
precision=self.markets[pair]['precision']['amount'], precision=self.markets[pair]['precision']['amount'],
@ -737,7 +736,7 @@ class Exchange:
def create_dry_run_order(self, pair: str, ordertype: str, side: str, amount: float, def create_dry_run_order(self, pair: str, ordertype: str, side: str, amount: float,
rate: float, leverage: float, params: Dict = {}) -> Dict[str, Any]: rate: float, leverage: float, params: Dict = {}) -> Dict[str, Any]:
order_id = f'dry_run_{side}_{datetime.now().timestamp()}' order_id = f'dry_run_{side}_{datetime.now().timestamp()}'
_amount = self._contracts_to_amount(pair, self.amount_to_precision(pair, amount)) _amount = self.amount_to_precision(pair, amount)
dry_order: Dict[str, Any] = { dry_order: Dict[str, Any] = {
'id': order_id, 'id': order_id,
'symbol': pair, 'symbol': pair,
@ -901,7 +900,7 @@ class Exchange:
try: try:
# Set the precision for amount and price(rate) as accepted by the exchange # Set the precision for amount and price(rate) as accepted by the exchange
amount = self.amount_to_precision(pair, amount) amount = self.amount_to_precision(pair, self._amount_to_contracts(pair, amount))
needs_price = (ordertype != 'market' needs_price = (ordertype != 'market'
or self._api.options.get("createMarketBuyOrderRequiresPrice", False)) or self._api.options.get("createMarketBuyOrderRequiresPrice", False))
rate_for_order = self.price_to_precision(pair, rate) if needs_price else None rate_for_order = self.price_to_precision(pair, rate) if needs_price else None
@ -1983,6 +1982,119 @@ class Exchange:
else: else:
return 0.0 return 0.0
@retrier
def get_liquidation_price(
self,
pair: str,
# Dry-run
open_rate: float, # Entry price of position
is_short: bool,
position: float, # Absolute value of position size
wallet_balance: float, # Or margin balance
mm_ex_1: float = 0.0, # (Binance) Cross only
upnl_ex_1: float = 0.0, # (Binance) Cross only
) -> Optional[float]:
"""
Set's the margin mode on the exchange to cross or isolated for a specific pair
:param pair: base/quote currency pair (e.g. "ADA/USDT")
"""
if self.trading_mode == TradingMode.SPOT:
return None
elif (self.collateral is None):
raise OperationalException(f'{self.name}.collateral must be set for liquidation_price')
elif (self.trading_mode != TradingMode.FUTURES and self.collateral != Collateral.ISOLATED):
raise OperationalException(
f"{self.name} does not support {self.collateral.value} {self.trading_mode.value}")
if self._config['dry_run'] or not self.exchange_has("fetchPositions"):
return self.dry_run_liquidation_price(
pair=pair,
open_rate=open_rate,
is_short=is_short,
position=position,
wallet_balance=wallet_balance,
mm_ex_1=mm_ex_1,
upnl_ex_1=upnl_ex_1
)
try:
positions = self._api.fetch_positions([pair])
if len(positions) > 0:
pos = positions[0]
return pos['liquidationPrice']
else:
return None
except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
f'Could not set margin mode due to {e.__class__.__name__}. Message: {e}') from e
except ccxt.BaseError as e:
raise OperationalException(e) from e
def get_maintenance_ratio_and_amt(
self,
pair: str,
nominal_value: Optional[float] = 0.0,
) -> Tuple[float, Optional[float]]:
"""
:return: The maintenance margin ratio and maintenance amount
"""
raise OperationalException(self.name + ' does not support leverage futures trading')
def dry_run_liquidation_price(
self,
pair: str,
open_rate: float, # Entry price of position
is_short: bool,
position: float, # Absolute value of position size
wallet_balance: float, # Or margin balance
mm_ex_1: float = 0.0, # (Binance) Cross only
upnl_ex_1: float = 0.0, # (Binance) Cross only
) -> Optional[float]:
"""
PERPETUAL:
gateio: https://www.gate.io/help/futures/perpetual/22160/calculation-of-liquidation-price
okex: 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: Entry price of position
:param is_short: True if the trade is a short, false otherwise
:param position: Absolute value of position size (in base currency)
:param trading_mode: SPOT, MARGIN, FUTURES, etc.
:param collateral: Either ISOLATED or CROSS
:param wallet_balance: Amount of collateral in the wallet being used to trade
Cross-Margin Mode: crossWalletBalance
Isolated-Margin Mode: isolatedWalletBalance
# * Not required by Gateio or OKX
:param mm_ex_1:
:param upnl_ex_1:
"""
market = self.markets[pair]
taker_fee_rate = market['taker']
mm_ratio, _ = self.get_maintenance_ratio_and_amt(pair, position)
if self.trading_mode == TradingMode.FUTURES and self.collateral == Collateral.ISOLATED:
if market['inverse']:
raise OperationalException(
"Freqtrade does not yet support inverse contracts")
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(
"Freqtrade only supports isolated futures for leverage trading")
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

@ -1,6 +1,6 @@
""" Gate.io exchange subclass """ """ Gate.io exchange subclass """
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.exceptions import OperationalException
@ -31,7 +31,7 @@ class Gateio(Exchange):
# TradingMode.SPOT always supported and not required in this list # TradingMode.SPOT always supported and not required in this list
# (TradingMode.MARGIN, Collateral.CROSS), # (TradingMode.MARGIN, Collateral.CROSS),
# (TradingMode.FUTURES, Collateral.CROSS), # (TradingMode.FUTURES, Collateral.CROSS),
# (TradingMode.FUTURES, Collateral.ISOLATED) (TradingMode.FUTURES, Collateral.ISOLATED)
] ]
def validate_ordertypes(self, order_types: Dict) -> None: def validate_ordertypes(self, order_types: Dict) -> None:
@ -40,3 +40,14 @@ class Gateio(Exchange):
if any(v == 'market' for k, v in order_types.items()): if any(v == 'market' for k, v in order_types.items()):
raise OperationalException( raise OperationalException(
f'Exchange {self.name} does not support market orders.') f'Exchange {self.name} does not support market orders.')
def get_maintenance_ratio_and_amt(
self,
pair: str,
nominal_value: Optional[float] = 0.0,
) -> Tuple[float, Optional[float]]:
"""
:return: The maintenance margin ratio and maintenance amount
"""
info = self.markets[pair]['info']
return (float(info['maintenance_rate']), None)

View File

@ -19,7 +19,7 @@ from freqtrade.edge import Edge
from freqtrade.enums import (Collateral, RPCMessageType, RunMode, SellType, SignalDirection, State, from freqtrade.enums import (Collateral, RPCMessageType, RunMode, SellType, SignalDirection, State,
TradingMode) TradingMode)
from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError, from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError,
InvalidOrderException, PricingError) InvalidOrderException, OperationalException, PricingError)
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds
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
@ -106,8 +106,8 @@ class FreqtradeBot(LoggingMixin):
self.trading_mode = TradingMode(self.config.get('trading_mode', 'spot')) self.trading_mode = TradingMode(self.config.get('trading_mode', 'spot'))
self.collateral_type: Optional[Collateral] = None self.collateral_type: Optional[Collateral] = None
if 'collateral_type' in self.config: if 'collateral' in self.config:
self.collateral_type = Collateral(self.config['collateral_type']) self.collateral_type = Collateral(self.config['collateral'])
self._schedule = Scheduler() self._schedule = Scheduler()
@ -606,29 +606,32 @@ class FreqtradeBot(LoggingMixin):
is_short: bool is_short: bool
) -> Tuple[float, Optional[float]]: ) -> Tuple[float, Optional[float]]:
interest_rate = 0.0
isolated_liq = None
# TODO-lev: Uncomment once liq and interest merged in
# if TradingMode == TradingMode.MARGIN: # if TradingMode == TradingMode.MARGIN:
# interest_rate = self.exchange.get_interest_rate( # interest_rate = self.exchange.get_interest_rate(
# pair=pair, # pair=pair,
# open_rate=open_rate, # open_rate=open_rate,
# is_short=is_short # is_short=is_short
# ) # )
if self.trading_mode == TradingMode.SPOT:
# if self.collateral_type == Collateral.ISOLATED: return (0.0, None)
elif (
# isolated_liq = liquidation_price( self.collateral_type == Collateral.ISOLATED and
# exchange_name=self.exchange.name, self.trading_mode == TradingMode.FUTURES
# trading_mode=self.trading_mode, ):
# open_rate=open_rate, wallet_balance = (amount * open_rate)/leverage
# amount=amount, isolated_liq = self.exchange.get_liquidation_price(
# leverage=leverage, pair=pair,
# is_short=is_short open_rate=open_rate,
# ) is_short=is_short,
position=amount,
return interest_rate, isolated_liq wallet_balance=wallet_balance,
mm_ex_1=0.0,
upnl_ex_1=0.0,
)
return (0.0, isolated_liq)
else:
raise OperationalException(
"Freqtrade only supports isolated futures for leverage trading")
def execute_entry( def execute_entry(
self, self,
@ -1174,8 +1177,8 @@ class FreqtradeBot(LoggingMixin):
max_timeouts = self.config.get('unfilledtimeout', {}).get('exit_timeout_count', 0) max_timeouts = self.config.get('unfilledtimeout', {}).get('exit_timeout_count', 0)
if not_closed and (fully_cancelled or self.strategy.ft_check_timed_out( if not_closed and (fully_cancelled or self.strategy.ft_check_timed_out(
time_method, trade, order, datetime.now(timezone.utc)) time_method, trade, order, datetime.now(timezone.utc))
): ):
if is_entering: if is_entering:
self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['TIMEOUT']) self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['TIMEOUT'])
else: else:

View File

@ -333,7 +333,7 @@ class LocalTrade():
for key in kwargs: for key in kwargs:
setattr(self, key, kwargs[key]) setattr(self, key, kwargs[key])
if self.isolated_liq: if self.isolated_liq:
self.set_isolated_liq(self.isolated_liq) self.set_isolated_liq(isolated_liq=self.isolated_liq)
self.recalc_open_trade_value() self.recalc_open_trade_value()
if self.trading_mode == TradingMode.MARGIN and self.interest_rate is None: if self.trading_mode == TradingMode.MARGIN and self.interest_rate is None:
raise OperationalException( raise OperationalException(

View File

@ -817,27 +817,42 @@ def get_markets():
'symbol': 'ETH/USDT', 'symbol': 'ETH/USDT',
'base': 'ETH', 'base': 'ETH',
'quote': 'USDT', 'quote': 'USDT',
'spot': True, 'settle': None,
'future': True, 'baseId': 'ETH',
'swap': True, 'quoteId': 'USDT',
'margin': True, 'settleId': None,
'type': 'spot', 'type': 'spot',
'spot': True,
'margin': True,
'swap': True,
'future': True,
'option': False,
'active': True,
'contract': None,
'linear': None,
'inverse': None,
'taker': 0.0006,
'maker': 0.0002,
'contractSize': None, 'contractSize': None,
'expiry': None,
'expiryDateTime': None,
'strike': None,
'optionType': None,
'precision': { 'precision': {
'amount': 8, 'amount': 8,
'price': 8 'price': 8,
}, },
'limits': { 'limits': {
'leverage': {
'min': 1,
'max': 100,
},
'amount': { 'amount': {
'min': 0.02214286, 'min': 0.02214286,
'max': None 'max': None,
}, },
'price': { 'price': {
'min': 1e-08, 'min': 1e-08,
'max': None
},
'leverage': {
'min': None,
'max': None, 'max': None,
}, },
'cost': { 'cost': {
@ -845,8 +860,9 @@ def get_markets():
'max': None, 'max': None,
}, },
}, },
'active': True, 'info': {
'info': {}, 'maintenance_rate': '0.005',
},
}, },
'LTC/USDT': { 'LTC/USDT': {
'id': 'USDT-LTC', 'id': 'USDT-LTC',
@ -860,6 +876,8 @@ def get_markets():
'margin': True, 'margin': True,
'type': 'spot', 'type': 'spot',
'contractSize': None, 'contractSize': None,
'taker': 0.0006,
'maker': 0.0002,
'precision': { 'precision': {
'amount': 8, 'amount': 8,
'price': 8 'price': 8
@ -892,6 +910,8 @@ def get_markets():
'active': True, 'active': True,
'spot': True, 'spot': True,
'type': 'spot', 'type': 'spot',
'taker': 0.0006,
'maker': 0.0002,
'precision': { 'precision': {
'price': 8, 'price': 8,
'amount': 8, 'amount': 8,
@ -923,6 +943,8 @@ def get_markets():
'active': True, 'active': True,
'spot': True, 'spot': True,
'type': 'spot', 'type': 'spot',
'taker': 0.0006,
'maker': 0.0002,
'precision': { 'precision': {
'price': 8, 'price': 8,
'amount': 8, 'amount': 8,
@ -955,6 +977,8 @@ def get_markets():
'spot': True, 'spot': True,
'type': 'spot', 'type': 'spot',
'contractSize': None, 'contractSize': None,
'taker': 0.0006,
'maker': 0.0002,
'precision': { 'precision': {
'price': 8, 'price': 8,
'amount': 8, 'amount': 8,
@ -1023,6 +1047,8 @@ def get_markets():
'spot': False, 'spot': False,
'type': 'swap', 'type': 'swap',
'contractSize': 0.01, 'contractSize': 0.01,
'taker': 0.0006,
'maker': 0.0002,
'precision': { 'precision': {
'amount': 8, 'amount': 8,
'price': 8 'price': 8
@ -1098,7 +1124,6 @@ def get_markets():
'swap': True, 'swap': True,
'futures': False, 'futures': False,
'option': False, 'option': False,
'derivative': True,
'contract': True, 'contract': True,
'linear': True, 'linear': True,
'inverse': False, 'inverse': False,

View File

@ -174,35 +174,35 @@ def test_get_max_leverage_binance(default_conf, mocker, pair, stake_amount, max_
exchange = get_patched_exchange(mocker, default_conf, id="binance") exchange = get_patched_exchange(mocker, default_conf, id="binance")
exchange._leverage_brackets = { exchange._leverage_brackets = {
'BNB/BUSD': [ 'BNB/BUSD': [
[0.0, 0.025], # lev = 40.0 [0.0, 0.025, 0.0], # lev = 40.0
[100000.0, 0.05], # lev = 20.0 [100000.0, 0.05, 2500.0], # lev = 20.0
[500000.0, 0.1], # lev = 10.0 [500000.0, 0.1, 27500.0], # lev = 10.0
[1000000.0, 0.15], # lev = 6.666666666666667 [1000000.0, 0.15, 77500.0], # lev = 6.666666666666667
[2000000.0, 0.25], # lev = 4.0 [2000000.0, 0.25, 277500.0], # lev = 4.0
[5000000.0, 0.5], # lev = 2.0 [5000000.0, 0.5, 1527500.0], # lev = 2.0
], ],
'BNB/USDT': [ 'BNB/USDT': [
[0.0, 0.0065], # lev = 153.84615384615384 [0.0, 0.0065, 0.0], # lev = 153.84615384615384
[10000.0, 0.01], # lev = 100.0 [10000.0, 0.01, 35.0], # lev = 100.0
[50000.0, 0.02], # lev = 50.0 [50000.0, 0.02, 535.0], # lev = 50.0
[250000.0, 0.05], # lev = 20.0 [250000.0, 0.05, 8035.0], # lev = 20.0
[1000000.0, 0.1], # lev = 10.0 [1000000.0, 0.1, 58035.0], # lev = 10.0
[2000000.0, 0.125], # lev = 8.0 [2000000.0, 0.125, 108035.0], # lev = 8.0
[5000000.0, 0.15], # lev = 6.666666666666667 [5000000.0, 0.15, 233035.0], # lev = 6.666666666666667
[10000000.0, 0.25], # lev = 4.0 [10000000.0, 0.25, 1233035.0], # lev = 4.0
], ],
'BTC/USDT': [ 'BTC/USDT': [
[0.0, 0.004], # lev = 250.0 [0.0, 0.004, 0.0], # lev = 250.0
[50000.0, 0.005], # lev = 200.0 [50000.0, 0.005, 50.0], # lev = 200.0
[250000.0, 0.01], # lev = 100.0 [250000.0, 0.01, 1300.0], # lev = 100.0
[1000000.0, 0.025], # lev = 40.0 [1000000.0, 0.025, 16300.0], # lev = 40.0
[5000000.0, 0.05], # lev = 20.0 [5000000.0, 0.05, 141300.0], # lev = 20.0
[20000000.0, 0.1], # lev = 10.0 [20000000.0, 0.1, 1141300.0], # lev = 10.0
[50000000.0, 0.125], # lev = 8.0 [50000000.0, 0.125, 2391300.0], # lev = 8.0
[100000000.0, 0.15], # lev = 6.666666666666667 [100000000.0, 0.15, 4891300.0], # lev = 6.666666666666667
[200000000.0, 0.25], # lev = 4.0 [200000000.0, 0.25, 24891300.0], # lev = 4.0
[300000000.0, 0.5], # lev = 2.0 [300000000.0, 0.5, 99891300.0], # lev = 2.0
], ]
} }
assert exchange.get_max_leverage(pair, stake_amount) == max_lev assert exchange.get_max_leverage(pair, stake_amount) == max_lev
@ -241,28 +241,28 @@ def test_fill_leverage_brackets_binance(default_conf, mocker):
exchange.fill_leverage_brackets() exchange.fill_leverage_brackets()
assert exchange._leverage_brackets == { assert exchange._leverage_brackets == {
'ADA/BUSD': [[0.0, 0.025], 'ADA/BUSD': [[0.0, 0.025, 0.0],
[100000.0, 0.05], [100000.0, 0.05, 2500.0],
[500000.0, 0.1], [500000.0, 0.1, 27500.0],
[1000000.0, 0.15], [1000000.0, 0.15, 77499.99999999999],
[2000000.0, 0.25], [2000000.0, 0.25, 277500.0],
[5000000.0, 0.5]], [5000000.0, 0.5, 1527500.0]],
'BTC/USDT': [[0.0, 0.004], 'BTC/USDT': [[0.0, 0.004, 0.0],
[50000.0, 0.005], [50000.0, 0.005, 50.0],
[250000.0, 0.01], [250000.0, 0.01, 1300.0],
[1000000.0, 0.025], [1000000.0, 0.025, 16300.000000000002],
[5000000.0, 0.05], [5000000.0, 0.05, 141300.0],
[20000000.0, 0.1], [20000000.0, 0.1, 1141300.0],
[50000000.0, 0.125], [50000000.0, 0.125, 2391300.0],
[100000000.0, 0.15], [100000000.0, 0.15, 4891300.0],
[200000000.0, 0.25], [200000000.0, 0.25, 24891300.0],
[300000000.0, 0.5]], [300000000.0, 0.5, 99891300.0]],
"ZEC/USDT": [[0.0, 0.01], "ZEC/USDT": [[0.0, 0.01, 0.0],
[5000.0, 0.025], [5000.0, 0.025, 75.0],
[25000.0, 0.05], [25000.0, 0.05, 700.0],
[100000.0, 0.1], [100000.0, 0.1, 5700.0],
[250000.0, 0.125], [250000.0, 0.125, 11949.999999999998],
[1000000.0, 0.5]], [1000000.0, 0.5, 386950.0]]
} }
api_mock = MagicMock() api_mock = MagicMock()
@ -288,37 +288,37 @@ def test_fill_leverage_brackets_binance_dryrun(default_conf, mocker):
leverage_brackets = { leverage_brackets = {
"1000SHIB/USDT": [ "1000SHIB/USDT": [
[0.0, 0.01], [0.0, 0.01, 0.0],
[5000.0, 0.025], [5000.0, 0.025, 75.0],
[25000.0, 0.05], [25000.0, 0.05, 700.0],
[100000.0, 0.1], [100000.0, 0.1, 5700.0],
[250000.0, 0.125], [250000.0, 0.125, 11949.999999999998],
[1000000.0, 0.5] [1000000.0, 0.5, 386950.0],
], ],
"1INCH/USDT": [ "1INCH/USDT": [
[0.0, 0.012], [0.0, 0.012, 0.0],
[5000.0, 0.025], [5000.0, 0.025, 65.0],
[25000.0, 0.05], [25000.0, 0.05, 690.0],
[100000.0, 0.1], [100000.0, 0.1, 5690.0],
[250000.0, 0.125], [250000.0, 0.125, 11939.999999999998],
[1000000.0, 0.5] [1000000.0, 0.5, 386940.0],
], ],
"AAVE/USDT": [ "AAVE/USDT": [
[0.0, 0.01], [0.0, 0.01, 0.0],
[50000.0, 0.02], [50000.0, 0.02, 500.0],
[250000.0, 0.05], [250000.0, 0.05, 8000.000000000001],
[1000000.0, 0.1], [1000000.0, 0.1, 58000.0],
[2000000.0, 0.125], [2000000.0, 0.125, 107999.99999999999],
[5000000.0, 0.1665], [5000000.0, 0.1665, 315500.00000000006],
[10000000.0, 0.25] [10000000.0, 0.25, 1150500.0],
], ],
"ADA/BUSD": [ "ADA/BUSD": [
[0.0, 0.025], [0.0, 0.025, 0.0],
[100000.0, 0.05], [100000.0, 0.05, 2500.0],
[500000.0, 0.1], [500000.0, 0.1, 27500.0],
[1000000.0, 0.15], [1000000.0, 0.15, 77499.99999999999],
[2000000.0, 0.25], [2000000.0, 0.25, 277500.0],
[5000000.0, 0.5] [5000000.0, 0.5, 1527500.0],
] ]
} }
@ -395,3 +395,51 @@ def test__ccxt_config(default_conf, mocker, trading_mode, collateral, config):
default_conf['collateral'] = collateral default_conf['collateral'] = collateral
exchange = get_patched_exchange(mocker, default_conf, id="binance") exchange = get_patched_exchange(mocker, default_conf, id="binance")
assert exchange._ccxt_config == config assert exchange._ccxt_config == config
@pytest.mark.parametrize('pair,nominal_value,mm_ratio,amt', [
("BNB/BUSD", 0.0, 0.025, 0),
("BNB/USDT", 100.0, 0.0065, 0),
("BTC/USDT", 170.30, 0.004, 0),
("BNB/BUSD", 999999.9, 0.1, 27500.0),
("BNB/USDT", 5000000.0, 0.15, 233035.0),
("BTC/USDT", 300000000.1, 0.5, 99891300.0),
])
def test_get_maintenance_ratio_and_amt_binance(
default_conf,
mocker,
pair,
nominal_value,
mm_ratio,
amt,
):
exchange = get_patched_exchange(mocker, default_conf, id="binance")
exchange._leverage_brackets = {
'BNB/BUSD': [[0.0, 0.025, 0.0],
[100000.0, 0.05, 2500.0],
[500000.0, 0.1, 27500.0],
[1000000.0, 0.15, 77500.0],
[2000000.0, 0.25, 277500.0],
[5000000.0, 0.5, 1527500.0]],
'BNB/USDT': [[0.0, 0.0065, 0.0],
[10000.0, 0.01, 35.0],
[50000.0, 0.02, 535.0],
[250000.0, 0.05, 8035.0],
[1000000.0, 0.1, 58035.0],
[2000000.0, 0.125, 108035.0],
[5000000.0, 0.15, 233035.0],
[10000000.0, 0.25, 1233035.0]],
'BTC/USDT': [[0.0, 0.004, 0.0],
[50000.0, 0.005, 50.0],
[250000.0, 0.01, 1300.0],
[1000000.0, 0.025, 16300.0],
[5000000.0, 0.05, 141300.0],
[20000000.0, 0.1, 1141300.0],
[50000000.0, 0.125, 2391300.0],
[100000000.0, 0.15, 4891300.0],
[200000000.0, 0.25, 24891300.0],
[300000000.0, 0.5, 99891300.0]
]
}
(result_ratio, result_amt) = exchange.get_maintenance_ratio_and_amt(pair, nominal_value)
assert (round(result_ratio, 8), round(result_amt, 8)) == (mm_ratio, amt)

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,
@ -236,8 +242,8 @@ def test_validate_order_time_in_force(default_conf, mocker, caplog):
(2.34559, 4, 0.001, 1, 2.345, 'spot'), (2.34559, 4, 0.001, 1, 2.345, 'spot'),
(2.9999, 4, 0.001, 1, 2.999, 'spot'), (2.9999, 4, 0.001, 1, 2.999, 'spot'),
(2.9909, 4, 0.001, 1, 2.990, 'spot'), (2.9909, 4, 0.001, 1, 2.990, 'spot'),
(2.9909, 4, 0.005, 0.01, 299.09, 'futures'), (2.9909, 4, 0.005, 0.01, 2.99, 'futures'),
(2.9999, 4, 0.005, 10, 0.295, 'futures'), (2.9999, 4, 0.005, 10, 2.995, 'futures'),
]) ])
def test_amount_to_precision( def test_amount_to_precision(
default_conf, default_conf,
@ -3438,30 +3444,35 @@ def test_set_margin_mode(mocker, default_conf, collateral):
("bittrex", TradingMode.FUTURES, Collateral.CROSS, True), ("bittrex", TradingMode.FUTURES, Collateral.CROSS, True),
("bittrex", TradingMode.FUTURES, Collateral.ISOLATED, True), ("bittrex", TradingMode.FUTURES, Collateral.ISOLATED, True),
("gateio", TradingMode.MARGIN, Collateral.ISOLATED, True), ("gateio", TradingMode.MARGIN, Collateral.ISOLATED, True),
("okex", TradingMode.SPOT, None, False),
("okex", TradingMode.MARGIN, Collateral.CROSS, True),
("okex", TradingMode.MARGIN, Collateral.ISOLATED, True),
("okex", TradingMode.FUTURES, Collateral.CROSS, True),
# TODO-lev: Remove once implemented ("binance", TradingMode.FUTURES, Collateral.ISOLATED, False),
("gateio", TradingMode.FUTURES, Collateral.ISOLATED, False),
# * Remove once implemented
("okex", TradingMode.FUTURES, Collateral.ISOLATED, True),
("binance", TradingMode.MARGIN, Collateral.CROSS, True), ("binance", TradingMode.MARGIN, Collateral.CROSS, True),
("binance", TradingMode.FUTURES, Collateral.CROSS, True), ("binance", TradingMode.FUTURES, Collateral.CROSS, True),
("binance", TradingMode.FUTURES, Collateral.ISOLATED, True),
("kraken", TradingMode.MARGIN, Collateral.CROSS, True), ("kraken", TradingMode.MARGIN, Collateral.CROSS, True),
("kraken", TradingMode.FUTURES, Collateral.CROSS, True), ("kraken", TradingMode.FUTURES, Collateral.CROSS, True),
("ftx", TradingMode.MARGIN, Collateral.CROSS, True), ("ftx", TradingMode.MARGIN, Collateral.CROSS, True),
("ftx", TradingMode.FUTURES, Collateral.CROSS, True), ("ftx", TradingMode.FUTURES, Collateral.CROSS, True),
("gateio", TradingMode.MARGIN, Collateral.CROSS, True), ("gateio", TradingMode.MARGIN, Collateral.CROSS, True),
("gateio", TradingMode.FUTURES, Collateral.CROSS, True), ("gateio", TradingMode.FUTURES, Collateral.CROSS, True),
("gateio", TradingMode.FUTURES, Collateral.ISOLATED, True),
# TODO-lev: Uncomment once implemented # * Uncomment once implemented
# ("okex", TradingMode.FUTURES, Collateral.ISOLATED, False),
# ("binance", TradingMode.MARGIN, Collateral.CROSS, False), # ("binance", TradingMode.MARGIN, Collateral.CROSS, False),
# ("binance", TradingMode.FUTURES, Collateral.CROSS, False), # ("binance", TradingMode.FUTURES, Collateral.CROSS, False),
# ("binance", TradingMode.FUTURES, Collateral.ISOLATED, False),
# ("kraken", TradingMode.MARGIN, Collateral.CROSS, False), # ("kraken", TradingMode.MARGIN, Collateral.CROSS, False),
# ("kraken", TradingMode.FUTURES, Collateral.CROSS, False), # ("kraken", TradingMode.FUTURES, Collateral.CROSS, False),
# ("ftx", TradingMode.MARGIN, Collateral.CROSS, False), # ("ftx", TradingMode.MARGIN, Collateral.CROSS, False),
# ("ftx", TradingMode.FUTURES, Collateral.CROSS, False), # ("ftx", TradingMode.FUTURES, Collateral.CROSS, False),
# ("gateio", TradingMode.MARGIN, Collateral.CROSS, False), # ("gateio", TradingMode.MARGIN, Collateral.CROSS, False),
# ("gateio", TradingMode.FUTURES, Collateral.CROSS, False), # ("gateio", TradingMode.FUTURES, Collateral.CROSS, False),
# ("gateio", TradingMode.FUTURES, Collateral.ISOLATED, False),
]) ])
def test_validate_trading_mode_and_collateral( def test_validate_trading_mode_and_collateral(
default_conf, default_conf,
@ -3582,6 +3593,68 @@ def test_calculate_funding_fees(
) == kraken_fee ) == kraken_fee
def test_get_liquidation_price(mocker, default_conf):
api_mock = MagicMock()
positions = [
{
'info': {},
'symbol': 'NEAR/USDT:USDT',
'timestamp': 1642164737148,
'datetime': '2022-01-14T12:52:17.148Z',
'initialMargin': 1.51072,
'initialMarginPercentage': 0.1,
'maintenanceMargin': 0.38916147,
'maintenanceMarginPercentage': 0.025,
'entryPrice': 18.884,
'notional': 15.1072,
'leverage': 9.97,
'unrealizedPnl': 0.0048,
'contracts': 8,
'contractSize': 0.1,
'marginRatio': None,
'liquidationPrice': 17.47,
'markPrice': 18.89,
'collateral': 1.52549075,
'marginType': 'isolated',
'side': 'buy',
'percentage': 0.003177292946409658
}
]
api_mock.fetch_positions = MagicMock(return_value=positions)
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
exchange_has=MagicMock(return_value=True),
)
default_conf['dry_run'] = False
default_conf['trading_mode'] = 'futures'
default_conf['collateral'] = 'isolated'
exchange = get_patched_exchange(mocker, default_conf, api_mock)
liq_price = exchange.get_liquidation_price(
pair='NEAR/USDT:USDT',
open_rate=0.0,
is_short=False,
position=0.0,
wallet_balance=0.0,
)
assert liq_price == 17.47
ccxt_exceptionhandlers(
mocker,
default_conf,
api_mock,
"binance",
"get_liquidation_price",
"fetch_positions",
pair="XRP/USDT",
open_rate=0.0,
is_short=False,
position=0.0,
wallet_balance=0.0,
)
@pytest.mark.parametrize('exchange,rate_start,rate_end,d1,d2,amount,expected_fees', [ @pytest.mark.parametrize('exchange,rate_start,rate_end,d1,d2,amount,expected_fees', [
('binance', 0, 2, "2021-09-01 00:00:00", "2021-09-01 08:00:00", 30.0, -0.0009140999999999999), ('binance', 0, 2, "2021-09-01 00:00:00", "2021-09-01 08:00:00", 30.0, -0.0009140999999999999),
('binance', 0, 2, "2021-09-01 00:00:15", "2021-09-01 08:00:00", 30.0, -0.0009140999999999999), ('binance', 0, 2, "2021-09-01 00:00:15", "2021-09-01 08:00:00", 30.0, -0.0009140999999999999),
@ -3622,41 +3695,41 @@ def test__fetch_and_calculate_funding_fees(
amount, amount,
expected_fees expected_fees
): ):
''' """
nominal_value = mark_price * size nominal_value = mark_price * size
funding_fee = nominal_value * funding_rate funding_fee = nominal_value * funding_rate
size: 30 size: 30
time: 0, mark: 2.77, nominal_value: 83.1, fundRate: -0.000008, fundFee: -0.0006648 time: 0, mark: 2.77, nominal_value: 83.1, fundRate: -0.000008, fundFee: -0.0006648
time: 1, mark: 2.73, nominal_value: 81.9, fundRate: -0.000004, fundFee: -0.0003276 time: 1, mark: 2.73, nominal_value: 81.9, fundRate: -0.000004, fundFee: -0.0003276
time: 2, mark: 2.74, nominal_value: 82.2, fundRate: 0.000012, fundFee: 0.0009864 time: 2, mark: 2.74, nominal_value: 82.2, fundRate: 0.000012, fundFee: 0.0009864
time: 3, mark: 2.76, nominal_value: 82.8, fundRate: -0.000003, fundFee: -0.0002484 time: 3, mark: 2.76, nominal_value: 82.8, fundRate: -0.000003, fundFee: -0.0002484
time: 4, mark: 2.76, nominal_value: 82.8, fundRate: -0.000007, fundFee: -0.0005796 time: 4, mark: 2.76, nominal_value: 82.8, fundRate: -0.000007, fundFee: -0.0005796
time: 5, mark: 2.77, nominal_value: 83.1, fundRate: 0.000003, fundFee: 0.0002493 time: 5, mark: 2.77, nominal_value: 83.1, fundRate: 0.000003, fundFee: 0.0002493
time: 6, mark: 2.78, nominal_value: 83.4, fundRate: 0.000019, fundFee: 0.0015846 time: 6, mark: 2.78, nominal_value: 83.4, fundRate: 0.000019, fundFee: 0.0015846
time: 7, mark: 2.78, nominal_value: 83.4, fundRate: 0.000003, fundFee: 0.0002502 time: 7, mark: 2.78, nominal_value: 83.4, fundRate: 0.000003, fundFee: 0.0002502
time: 8, mark: 2.77, nominal_value: 83.1, fundRate: -0.000003, fundFee: -0.0002493 time: 8, mark: 2.77, nominal_value: 83.1, fundRate: -0.000003, fundFee: -0.0002493
time: 9, mark: 2.77, nominal_value: 83.1, fundRate: 0, fundFee: 0.0 time: 9, mark: 2.77, nominal_value: 83.1, fundRate: 0, fundFee: 0.0
time: 10, mark: 2.84, nominal_value: 85.2, fundRate: 0.000013, fundFee: 0.0011076 time: 10, mark: 2.84, nominal_value: 85.2, fundRate: 0.000013, fundFee: 0.0011076
time: 11, mark: 2.81, nominal_value: 84.3, fundRate: 0.000077, fundFee: 0.0064911 time: 11, mark: 2.81, nominal_value: 84.3, fundRate: 0.000077, fundFee: 0.0064911
time: 12, mark: 2.81, nominal_value: 84.3, fundRate: 0.000072, fundFee: 0.0060696 time: 12, mark: 2.81, nominal_value: 84.3, fundRate: 0.000072, fundFee: 0.0060696
time: 13, mark: 2.82, nominal_value: 84.6, fundRate: 0.000097, fundFee: 0.0082062 time: 13, mark: 2.82, nominal_value: 84.6, fundRate: 0.000097, fundFee: 0.0082062
size: 50 size: 50
time: 0, mark: 2.77, nominal_value: 138.5, fundRate: -0.000008, fundFee: -0.001108 time: 0, mark: 2.77, nominal_value: 138.5, fundRate: -0.000008, fundFee: -0.001108
time: 1, mark: 2.73, nominal_value: 136.5, fundRate: -0.000004, fundFee: -0.000546 time: 1, mark: 2.73, nominal_value: 136.5, fundRate: -0.000004, fundFee: -0.000546
time: 2, mark: 2.74, nominal_value: 137.0, fundRate: 0.000012, fundFee: 0.001644 time: 2, mark: 2.74, nominal_value: 137.0, fundRate: 0.000012, fundFee: 0.001644
time: 3, mark: 2.76, nominal_value: 138.0, fundRate: -0.000003, fundFee: -0.000414 time: 3, mark: 2.76, nominal_value: 138.0, fundRate: -0.000003, fundFee: -0.000414
time: 4, mark: 2.76, nominal_value: 138.0, fundRate: -0.000007, fundFee: -0.000966 time: 4, mark: 2.76, nominal_value: 138.0, fundRate: -0.000007, fundFee: -0.000966
time: 5, mark: 2.77, nominal_value: 138.5, fundRate: 0.000003, fundFee: 0.0004155 time: 5, mark: 2.77, nominal_value: 138.5, fundRate: 0.000003, fundFee: 0.0004155
time: 6, mark: 2.78, nominal_value: 139.0, fundRate: 0.000019, fundFee: 0.002641 time: 6, mark: 2.78, nominal_value: 139.0, fundRate: 0.000019, fundFee: 0.002641
time: 7, mark: 2.78, nominal_value: 139.0, fundRate: 0.000003, fundFee: 0.000417 time: 7, mark: 2.78, nominal_value: 139.0, fundRate: 0.000003, fundFee: 0.000417
time: 8, mark: 2.77, nominal_value: 138.5, fundRate: -0.000003, fundFee: -0.0004155 time: 8, mark: 2.77, nominal_value: 138.5, fundRate: -0.000003, fundFee: -0.0004155
time: 9, mark: 2.77, nominal_value: 138.5, fundRate: 0, fundFee: 0.0 time: 9, mark: 2.77, nominal_value: 138.5, fundRate: 0, fundFee: 0.0
time: 10, mark: 2.84, nominal_value: 142.0, fundRate: 0.000013, fundFee: 0.001846 time: 10, mark: 2.84, nominal_value: 142.0, fundRate: 0.000013, fundFee: 0.001846
time: 11, mark: 2.81, nominal_value: 140.5, fundRate: 0.000077, fundFee: 0.0108185 time: 11, mark: 2.81, nominal_value: 140.5, fundRate: 0.000077, fundFee: 0.0108185
time: 12, mark: 2.81, nominal_value: 140.5, fundRate: 0.000072, fundFee: 0.010116 time: 12, mark: 2.81, nominal_value: 140.5, fundRate: 0.000072, fundFee: 0.010116
time: 13, mark: 2.82, nominal_value: 141.0, fundRate: 0.000097, fundFee: 0.013677 time: 13, mark: 2.82, nominal_value: 141.0, fundRate: 0.000097, fundFee: 0.013677
''' """
d1 = datetime.strptime(f"{d1} +0000", '%Y-%m-%d %H:%M:%S %z') d1 = datetime.strptime(f"{d1} +0000", '%Y-%m-%d %H:%M:%S %z')
d2 = datetime.strptime(f"{d2} +0000", '%Y-%m-%d %H:%M:%S %z') d2 = datetime.strptime(f"{d2} +0000", '%Y-%m-%d %H:%M:%S %z')
funding_rate_history = { funding_rate_history = {
@ -3909,3 +3982,69 @@ 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,trading_mode,collateral', [
# Bittrex
('bittrex', 2.0, False, 'spot', None),
('bittrex', 2.0, False, 'spot', 'cross'),
('bittrex', 2.0, True, 'spot', 'isolated'),
# Binance
('binance', 2.0, False, 'spot', None),
('binance', 2.0, False, 'spot', 'cross'),
('binance', 2.0, True, 'spot', 'isolated'),
])
def test_liquidation_price_is_none(
mocker,
default_conf,
exchange_name,
open_rate,
is_short,
trading_mode,
collateral
):
default_conf['trading_mode'] = trading_mode
default_conf['collateral'] = collateral
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
assert exchange.get_liquidation_price(
pair='DOGE/USDT',
open_rate=open_rate,
is_short=is_short,
position=71200.81144,
wallet_balance=-56354.57,
mm_ex_1=0.10,
upnl_ex_1=0.0
) is None
@pytest.mark.parametrize(
'exchange_name, is_short, trading_mode, collateral, wallet_balance, '
'mm_ex_1, upnl_ex_1, maintenance_amt, position, open_rate, '
'mm_ratio, expected',
[
("binance", False, 'futures', 'isolated', 1535443.01, 0.0,
0.0, 135365.00, 3683.979, 1456.84, 0.10, 1114.78),
("binance", False, 'futures', 'isolated', 1535443.01, 0.0,
0.0, 16300.000, 109.488, 32481.980, 0.025, 18778.73),
("binance", False, 'futures', 'cross', 1535443.01, 71200.81144,
-56354.57, 135365.00, 3683.979, 1456.84, 0.10, 1153.26),
("binance", False, '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, trading_mode,
collateral, wallet_balance, mm_ex_1, upnl_ex_1, maintenance_amt, position, mm_ratio, expected
):
default_conf['trading_mode'] = trading_mode
default_conf['collateral'] = collateral
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
exchange.get_maintenance_ratio_and_amt = MagicMock(return_value=(mm_ratio, maintenance_amt))
assert isclose(round(exchange.get_liquidation_price(
pair='DOGE/USDT',
open_rate=open_rate,
is_short=is_short,
wallet_balance=wallet_balance,
mm_ex_1=mm_ex_1,
upnl_ex_1=upnl_ex_1,
position=position,
), 2), expected)

View File

@ -1,8 +1,11 @@
from unittest.mock import MagicMock, PropertyMock
import pytest import pytest
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.exchange import Gateio from freqtrade.exchange import Gateio
from freqtrade.resolvers.exchange_resolver import ExchangeResolver from freqtrade.resolvers.exchange_resolver import ExchangeResolver
from tests.conftest import get_patched_exchange
def test_validate_order_types_gateio(default_conf, mocker): def test_validate_order_types_gateio(default_conf, mocker):
@ -26,3 +29,38 @@ def test_validate_order_types_gateio(default_conf, mocker):
with pytest.raises(OperationalException, with pytest.raises(OperationalException,
match=r'Exchange .* does not support market orders.'): match=r'Exchange .* does not support market orders.'):
ExchangeResolver.load_exchange('gateio', default_conf, True) ExchangeResolver.load_exchange('gateio', default_conf, True)
@pytest.mark.parametrize('pair,mm_ratio', [
("ETH/USDT:USDT", 0.005),
("ADA/USDT:USDT", 0.003),
])
def test_get_maintenance_ratio_and_amt_gateio(default_conf, mocker, pair, mm_ratio):
api_mock = MagicMock()
exchange = get_patched_exchange(mocker, default_conf, api_mock, id="gateio")
mocker.patch(
'freqtrade.exchange.Exchange.markets',
PropertyMock(
return_value={
'ETH/USDT:USDT': {
'taker': 0.0000075,
'maker': -0.0000025,
'info': {
'maintenance_rate': '0.005',
},
'id': 'ETH_USDT',
'symbol': 'ETH/USDT:USDT',
},
'ADA/USDT:USDT': {
'taker': 0.0000075,
'maker': -0.0000025,
'info': {
'maintenance_rate': '0.003',
},
'id': 'ADA_USDT',
'symbol': 'ADA/USDT:USDT',
},
}
)
)
assert exchange.get_maintenance_ratio_and_amt(pair) == (mm_ratio, None)

View File

@ -707,23 +707,52 @@ def test_process_informative_pairs_added(default_conf_usdt, ticker_usdt, mocker)
CandleType.SPOT) in refresh_mock.call_args[0][0] CandleType.SPOT) in refresh_mock.call_args[0][0]
@pytest.mark.parametrize("trading_mode", [ @pytest.mark.parametrize("is_short,trading_mode,exchange_name,margin_mode,liq_price", [
'spot', (False, 'spot', 'binance', None, None),
# TODO-lev: Enable other modes (True, 'spot', 'binance', None, None),
# 'margin', 'futures' (False, 'spot', 'gateio', None, None),
] (True, 'spot', 'gateio', None, None),
) (False, 'spot', 'okex', None, None),
@pytest.mark.parametrize("is_short", [False, True]) (True, 'spot', 'okex', None, None),
(True, 'futures', 'binance', 'isolated', 11.89108910891089),
(False, 'futures', 'binance', 'isolated', 8.070707070707071),
(True, 'futures', 'gateio', 'isolated', 11.87413417771621),
(False, 'futures', 'gateio', 'isolated', 8.085708510208207),
# (True, 'futures', 'okex', 'isolated', 11.87413417771621),
# (False, 'futures', 'okex', 'isolated', 8.085708510208207),
])
def test_execute_entry(mocker, default_conf_usdt, fee, limit_order, def test_execute_entry(mocker, default_conf_usdt, fee, limit_order,
limit_order_open, is_short, trading_mode) -> None: limit_order_open, is_short, trading_mode,
exchange_name, margin_mode, liq_price) -> None:
"""
exchange_name = binance, is_short = true
leverage = 5
position = 0.2 * 5
((wb + cum_b) - (side_1 * position * ep1)) / ((position * mmr_b) - (side_1 * position))
((2 + 0.01) - ((-1) * 1 * 10)) / ((1 * 0.01) - ((-1) * 1)) = 11.89108910891089
exchange_name = binance, is_short = false
((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
exchange_name = gateio/okex, is_short = true
(open_rate + (wallet_balance / position)) / (1 + (mm_ratio + taker_fee_rate))
(10 + (2 / 1)) / (1 + (0.01 + 0.0006)) = 11.87413417771621
exchange_name = gateio/okex, is_short = false
(open_rate - (wallet_balance / position)) / (1 - (mm_ratio + taker_fee_rate))
(10 - (2 / 1)) / (1 - (0.01 + 0.0006)) = 8.085708510208207
"""
open_order = limit_order_open[enter_side(is_short)] open_order = limit_order_open[enter_side(is_short)]
order = limit_order[enter_side(is_short)] order = limit_order[enter_side(is_short)]
default_conf_usdt['trading_mode'] = trading_mode default_conf_usdt['trading_mode'] = trading_mode
leverage = 1.0 if trading_mode == 'spot' else 3.0 leverage = 1.0 if trading_mode == 'spot' else 5.0
default_conf_usdt['collateral'] = 'cross' default_conf_usdt['exchange']['name'] = exchange_name
if margin_mode:
default_conf_usdt['collateral'] = margin_mode
mocker.patch('freqtrade.exchange.Gateio.validate_ordertypes')
patch_RPCManager(mocker) patch_RPCManager(mocker)
patch_exchange(mocker) patch_exchange(mocker, id=exchange_name)
freqtrade = FreqtradeBot(default_conf_usdt) freqtrade = FreqtradeBot(default_conf_usdt)
freqtrade.strategy.confirm_trade_entry = MagicMock(return_value=False) freqtrade.strategy.confirm_trade_entry = MagicMock(return_value=False)
freqtrade.strategy.leverage = MagicMock(return_value=leverage) freqtrade.strategy.leverage = MagicMock(return_value=leverage)
@ -743,6 +772,7 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order,
get_min_pair_stake_amount=MagicMock(return_value=1), get_min_pair_stake_amount=MagicMock(return_value=1),
get_fee=fee, get_fee=fee,
get_funding_fees=MagicMock(return_value=0), get_funding_fees=MagicMock(return_value=0),
name=exchange_name
) )
pair = 'ETH/USDT' pair = 'ETH/USDT'
@ -886,14 +916,21 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order,
assert trade.open_rate_requested == 10 assert trade.open_rate_requested == 10
# In case of custom entry price not float type # In case of custom entry price not float type
freqtrade.exchange.get_maintenance_ratio_and_amt = MagicMock(return_value=(0.01, 0.01))
freqtrade.exchange.name = exchange_name
order['status'] = 'open' order['status'] = 'open'
order['id'] = '5568' order['id'] = '5568'
freqtrade.strategy.custom_entry_price = lambda **kwargs: "string price" freqtrade.strategy.custom_entry_price = lambda **kwargs: "string price"
assert freqtrade.execute_entry(pair, stake_amount, is_short=is_short) assert freqtrade.execute_entry(pair, stake_amount, is_short=is_short)
trade = Trade.query.all()[8] trade = Trade.query.all()[8]
# Trade(id=9, pair=ETH/USDT, amount=0.20000000, is_short=False,
# leverage=1.0, open_rate=10.00000000, open_since=...)
# Trade(id=9, pair=ETH/USDT, amount=0.60000000, is_short=True,
# leverage=3.0, open_rate=10.00000000, open_since=...)
trade.is_short = is_short trade.is_short = is_short
assert trade assert trade
assert trade.open_rate_requested == 10 assert trade.open_rate_requested == 10
assert trade.isolated_liq == liq_price
@pytest.mark.parametrize("is_short", [False, True]) @pytest.mark.parametrize("is_short", [False, True])
@ -4794,23 +4831,23 @@ def test_update_funding_fees(
limit_order_open, limit_order_open,
schedule_off schedule_off
): ):
''' """
nominal_value = mark_price * size nominal_value = mark_price * size
funding_fee = nominal_value * funding_rate funding_fee = nominal_value * funding_rate
size = 123 size = 123
"LTC/BTC" "LTC/USDT"
time: 0, mark: 3.3, fundRate: 0.00032583, nominal_value: 405.9, fundFee: 0.132254397 time: 0, mark: 3.3, fundRate: 0.00032583, nominal_value: 405.9, fundFee: 0.132254397
time: 8, mark: 3.2, fundRate: 0.00024472, nominal_value: 393.6, fundFee: 0.096321792 time: 8, mark: 3.2, fundRate: 0.00024472, nominal_value: 393.6, fundFee: 0.096321792
"ETH/BTC" "ETH/USDT"
time: 0, mark: 2.4, fundRate: 0.0001, nominal_value: 295.2, fundFee: 0.02952 time: 0, mark: 2.4, fundRate: 0.0001, nominal_value: 295.2, fundFee: 0.02952
time: 8, mark: 2.5, fundRate: 0.0001, nominal_value: 307.5, fundFee: 0.03075 time: 8, mark: 2.5, fundRate: 0.0001, nominal_value: 307.5, fundFee: 0.03075
"ETC/BTC" "ETC/USDT"
time: 0, mark: 4.3, fundRate: 0.00031077, nominal_value: 528.9, fundFee: 0.164366253 time: 0, mark: 4.3, fundRate: 0.00031077, nominal_value: 528.9, fundFee: 0.164366253
time: 8, mark: 4.1, fundRate: 0.00022655, nominal_value: 504.3, fundFee: 0.114249165 time: 8, mark: 4.1, fundRate: 0.00022655, nominal_value: 504.3, fundFee: 0.114249165
"XRP/BTC" "XRP/USDT"
time: 0, mark: 1.2, fundRate: 0.00049426, nominal_value: 147.6, fundFee: 0.072952776 time: 0, mark: 1.2, fundRate: 0.00049426, nominal_value: 147.6, fundFee: 0.072952776
time: 8, mark: 1.2, fundRate: 0.00032715, nominal_value: 147.6, fundFee: 0.04828734 time: 8, mark: 1.2, fundRate: 0.00032715, nominal_value: 147.6, fundFee: 0.04828734
''' """
# SETUP # SETUP
time_machine.move_to("2021-09-01 00:00:00 +00:00") time_machine.move_to("2021-09-01 00:00:00 +00:00")
@ -4831,19 +4868,19 @@ def test_update_funding_fees(
# 16:00 entry is actually never used # 16:00 entry is actually never used
# But should be kept in the test to ensure we're filtering correctly. # But should be kept in the test to ensure we're filtering correctly.
funding_rates = { funding_rates = {
"LTC/BTC": "LTC/USDT":
DataFrame([ DataFrame([
[date_midnight, 0.00032583, 0, 0, 0, 0], [date_midnight, 0.00032583, 0, 0, 0, 0],
[date_eight, 0.00024472, 0, 0, 0, 0], [date_eight, 0.00024472, 0, 0, 0, 0],
[date_sixteen, 0.00024472, 0, 0, 0, 0], [date_sixteen, 0.00024472, 0, 0, 0, 0],
], columns=columns), ], columns=columns),
"ETH/BTC": "ETH/USDT":
DataFrame([ DataFrame([
[date_midnight, 0.0001, 0, 0, 0, 0], [date_midnight, 0.0001, 0, 0, 0, 0],
[date_eight, 0.0001, 0, 0, 0, 0], [date_eight, 0.0001, 0, 0, 0, 0],
[date_sixteen, 0.0001, 0, 0, 0, 0], [date_sixteen, 0.0001, 0, 0, 0, 0],
], columns=columns), ], columns=columns),
"XRP/BTC": "XRP/USDT":
DataFrame([ DataFrame([
[date_midnight, 0.00049426, 0, 0, 0, 0], [date_midnight, 0.00049426, 0, 0, 0, 0],
[date_eight, 0.00032715, 0, 0, 0, 0], [date_eight, 0.00032715, 0, 0, 0, 0],
@ -4852,19 +4889,19 @@ def test_update_funding_fees(
} }
mark_prices = { mark_prices = {
"LTC/BTC": "LTC/USDT":
DataFrame([ DataFrame([
[date_midnight, 3.3, 0, 0, 0, 0], [date_midnight, 3.3, 0, 0, 0, 0],
[date_eight, 3.2, 0, 0, 0, 0], [date_eight, 3.2, 0, 0, 0, 0],
[date_sixteen, 3.2, 0, 0, 0, 0], [date_sixteen, 3.2, 0, 0, 0, 0],
], columns=columns), ], columns=columns),
"ETH/BTC": "ETH/USDT":
DataFrame([ DataFrame([
[date_midnight, 2.4, 0, 0, 0, 0], [date_midnight, 2.4, 0, 0, 0, 0],
[date_eight, 2.5, 0, 0, 0, 0], [date_eight, 2.5, 0, 0, 0, 0],
[date_sixteen, 2.5, 0, 0, 0, 0], [date_sixteen, 2.5, 0, 0, 0, 0],
], columns=columns), ], columns=columns),
"XRP/BTC": "XRP/USDT":
DataFrame([ DataFrame([
[date_midnight, 1.2, 0, 0, 0, 0], [date_midnight, 1.2, 0, 0, 0, 0],
[date_eight, 1.2, 0, 0, 0, 0], [date_eight, 1.2, 0, 0, 0, 0],
@ -4901,9 +4938,9 @@ def test_update_funding_fees(
freqtrade = get_patched_freqtradebot(mocker, default_conf) freqtrade = get_patched_freqtradebot(mocker, default_conf)
# initial funding fees, # initial funding fees,
freqtrade.execute_entry('ETH/BTC', 123, is_short=is_short) freqtrade.execute_entry('ETH/USDT', 123, is_short=is_short)
freqtrade.execute_entry('LTC/BTC', 2.0, is_short=is_short) freqtrade.execute_entry('LTC/USDT', 2.0, is_short=is_short)
freqtrade.execute_entry('XRP/BTC', 123, is_short=is_short) freqtrade.execute_entry('XRP/USDT', 123, is_short=is_short)
multipl = 1 if is_short else -1 multipl = 1 if is_short else -1
trades = Trade.get_open_trades() trades = Trade.get_open_trades()
assert len(trades) == 3 assert len(trades) == 3

View File

@ -148,7 +148,7 @@ def test_set_stop_loss_isolated_liq(fee):
trade.stop_loss = None trade.stop_loss = None
trade.initial_stop_loss = None trade.initial_stop_loss = None
trade.set_isolated_liq(isolated_liq=0.09) trade.set_isolated_liq(0.09)
assert trade.isolated_liq == 0.09 assert trade.isolated_liq == 0.09
assert trade.stop_loss == 0.09 assert trade.stop_loss == 0.09
assert trade.initial_stop_loss == 0.09 assert trade.initial_stop_loss == 0.09
@ -158,12 +158,12 @@ def test_set_stop_loss_isolated_liq(fee):
assert trade.stop_loss == 0.08 assert trade.stop_loss == 0.08
assert trade.initial_stop_loss == 0.09 assert trade.initial_stop_loss == 0.09
trade.set_isolated_liq(isolated_liq=0.1) trade.set_isolated_liq(0.1)
assert trade.isolated_liq == 0.1 assert trade.isolated_liq == 0.1
assert trade.stop_loss == 0.08 assert trade.stop_loss == 0.08
assert trade.initial_stop_loss == 0.09 assert trade.initial_stop_loss == 0.09
trade.set_isolated_liq(isolated_liq=0.07) trade.set_isolated_liq(0.07)
assert trade.isolated_liq == 0.07 assert trade.isolated_liq == 0.07
assert trade.stop_loss == 0.07 assert trade.stop_loss == 0.07
assert trade.initial_stop_loss == 0.09 assert trade.initial_stop_loss == 0.09