diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 0f841e2a7..434734ef0 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -155,7 +155,7 @@ CONF_SCHEMA = { 'ignore_roi_if_buy_signal': {'type': 'boolean'}, 'ignore_buying_expired_candle_after': {'type': 'number'}, 'trading_mode': {'type': 'string', 'enum': TRADING_MODES}, - 'collateral_type': {'type': 'string', 'enum': COLLATERAL_TYPES}, + 'collateral': {'type': 'string', 'enum': COLLATERAL_TYPES}, 'backtest_breakdown': { 'type': 'array', 'items': {'type': 'string', 'enum': BACKTEST_BREAKDOWNS} diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index a922d9c0d..826087aac 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -33,10 +33,9 @@ class Binance(Exchange): _supported_trading_mode_collateral_pairs: List[Tuple[TradingMode, Collateral]] = [ # TradingMode.SPOT always supported and not required in this list - # TODO-lev: Uncomment once supported # (TradingMode.MARGIN, 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: @@ -120,10 +119,25 @@ class Binance(Exchange): raise OperationalException(e) from e @retrier - def fill_leverage_brackets(self): + def fill_leverage_brackets(self) -> None: """ Assigns property _leverage_brackets to a dictionary of information about the leverage 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: try: @@ -136,17 +150,21 @@ class Binance(Exchange): else: leverage_brackets = self._api.load_leverage_brackets() - for pair, brackets in leverage_brackets.items(): - self._leverage_brackets[pair] = [ - [ - min_amount, - float(margin_req) - ] for [ - min_amount, - margin_req - ] in brackets - ] - + for pair, brkts in leverage_brackets.items(): + [amt, old_ratio] = [0.0, 0.0] + brackets = [] + for [notional_floor, mm_ratio] in brkts: + amt = ( + (float(notional_floor) * (float(mm_ratio) - float(old_ratio))) + + amt + ) if old_ratio else 0.0 + old_ratio = mm_ratio + brackets.append([ + float(notional_floor), + float(mm_ratio), + amt, + ]) + self._leverage_brackets[pair] = brackets except ccxt.DDoSProtection as e: raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: @@ -161,16 +179,22 @@ class Binance(Exchange): :param pair: The base/quote currency pair being traded :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: return 1.0 pair_brackets = self._leverage_brackets[pair] num_brackets = len(pair_brackets) - min_amount = 0 + min_amount = 0.0 for bracket_num in range(num_brackets): - [_, margin_req] = pair_brackets[bracket_num] - lev = 1/margin_req + [notional_floor, mm_ratio, _] = pair_brackets[bracket_num] + 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 - [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: return 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 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") diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index e737d2b2a..4539ab352 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -90,7 +90,7 @@ class Exchange: self._api: ccxt.Exchange = None self._api_async: ccxt_async.Exchange = None self._markets: Dict = {} - self._leverage_brackets: Dict = {} + self._leverage_brackets: Dict[str, List[List[float]]] = {} self.loop = asyncio.new_event_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 based on our definitions. """ - amount = self._amount_to_contracts(pair, amount) if self.markets[pair]['precision']['amount']: amount = float(decimal_to_precision(amount, rounding_mode=TRUNCATE, 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, rate: float, leverage: float, params: Dict = {}) -> Dict[str, Any]: 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] = { 'id': order_id, 'symbol': pair, @@ -901,7 +900,7 @@ class Exchange: try: # 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' or self._api.options.get("createMarketBuyOrderRequiresPrice", False)) rate_for_order = self.price_to_precision(pair, rate) if needs_price else None @@ -1983,6 +1982,119 @@ class Exchange: else: 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: return exchange_name in ccxt_exchanges(ccxt_module) diff --git a/freqtrade/exchange/gateio.py b/freqtrade/exchange/gateio.py index fe4227d80..c62b6222d 100644 --- a/freqtrade/exchange/gateio.py +++ b/freqtrade/exchange/gateio.py @@ -1,6 +1,6 @@ """ Gate.io exchange subclass """ import logging -from typing import Dict, List, Tuple +from typing import Dict, List, Optional, Tuple from freqtrade.enums import Collateral, TradingMode from freqtrade.exceptions import OperationalException @@ -31,7 +31,7 @@ class Gateio(Exchange): # TradingMode.SPOT always supported and not required in this list # (TradingMode.MARGIN, Collateral.CROSS), # (TradingMode.FUTURES, Collateral.CROSS), - # (TradingMode.FUTURES, Collateral.ISOLATED) + (TradingMode.FUTURES, Collateral.ISOLATED) ] 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()): raise OperationalException( 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) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 113c78a15..ad5bc0fb6 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -19,7 +19,7 @@ from freqtrade.edge import Edge from freqtrade.enums import (Collateral, RPCMessageType, RunMode, SellType, SignalDirection, State, TradingMode) from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError, - InvalidOrderException, PricingError) + InvalidOrderException, OperationalException, PricingError) from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from freqtrade.misc import safe_value_fallback, safe_value_fallback2 from freqtrade.mixins import LoggingMixin @@ -106,8 +106,8 @@ class FreqtradeBot(LoggingMixin): self.trading_mode = TradingMode(self.config.get('trading_mode', 'spot')) self.collateral_type: Optional[Collateral] = None - if 'collateral_type' in self.config: - self.collateral_type = Collateral(self.config['collateral_type']) + if 'collateral' in self.config: + self.collateral_type = Collateral(self.config['collateral']) self._schedule = Scheduler() @@ -606,29 +606,32 @@ class FreqtradeBot(LoggingMixin): is_short: bool ) -> Tuple[float, Optional[float]]: - interest_rate = 0.0 - isolated_liq = None - - # TODO-lev: Uncomment once liq and interest merged in # if TradingMode == TradingMode.MARGIN: # interest_rate = self.exchange.get_interest_rate( # pair=pair, # open_rate=open_rate, # is_short=is_short # ) - - # if self.collateral_type == Collateral.ISOLATED: - - # isolated_liq = liquidation_price( - # exchange_name=self.exchange.name, - # trading_mode=self.trading_mode, - # open_rate=open_rate, - # amount=amount, - # leverage=leverage, - # is_short=is_short - # ) - - return interest_rate, isolated_liq + if self.trading_mode == TradingMode.SPOT: + return (0.0, None) + elif ( + self.collateral_type == Collateral.ISOLATED and + self.trading_mode == TradingMode.FUTURES + ): + wallet_balance = (amount * open_rate)/leverage + isolated_liq = self.exchange.get_liquidation_price( + pair=pair, + open_rate=open_rate, + is_short=is_short, + position=amount, + 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( self, @@ -1174,8 +1177,8 @@ class FreqtradeBot(LoggingMixin): max_timeouts = self.config.get('unfilledtimeout', {}).get('exit_timeout_count', 0) 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: self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['TIMEOUT']) else: diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 7c65fbc86..afee0725f 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -333,7 +333,7 @@ class LocalTrade(): for key in kwargs: setattr(self, key, kwargs[key]) 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() if self.trading_mode == TradingMode.MARGIN and self.interest_rate is None: raise OperationalException( diff --git a/tests/conftest.py b/tests/conftest.py index 207f6ae24..de1f44e89 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -817,27 +817,42 @@ def get_markets(): 'symbol': 'ETH/USDT', 'base': 'ETH', 'quote': 'USDT', - 'spot': True, - 'future': True, - 'swap': True, - 'margin': True, + 'settle': None, + 'baseId': 'ETH', + 'quoteId': 'USDT', + 'settleId': None, '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, + 'expiry': None, + 'expiryDateTime': None, + 'strike': None, + 'optionType': None, 'precision': { 'amount': 8, - 'price': 8 + 'price': 8, }, 'limits': { + 'leverage': { + 'min': 1, + 'max': 100, + }, 'amount': { 'min': 0.02214286, - 'max': None + 'max': None, }, 'price': { 'min': 1e-08, - 'max': None - }, - 'leverage': { - 'min': None, 'max': None, }, 'cost': { @@ -845,8 +860,9 @@ def get_markets(): 'max': None, }, }, - 'active': True, - 'info': {}, + 'info': { + 'maintenance_rate': '0.005', + }, }, 'LTC/USDT': { 'id': 'USDT-LTC', @@ -860,6 +876,8 @@ def get_markets(): 'margin': True, 'type': 'spot', 'contractSize': None, + 'taker': 0.0006, + 'maker': 0.0002, 'precision': { 'amount': 8, 'price': 8 @@ -892,6 +910,8 @@ def get_markets(): 'active': True, 'spot': True, 'type': 'spot', + 'taker': 0.0006, + 'maker': 0.0002, 'precision': { 'price': 8, 'amount': 8, @@ -923,6 +943,8 @@ def get_markets(): 'active': True, 'spot': True, 'type': 'spot', + 'taker': 0.0006, + 'maker': 0.0002, 'precision': { 'price': 8, 'amount': 8, @@ -955,6 +977,8 @@ def get_markets(): 'spot': True, 'type': 'spot', 'contractSize': None, + 'taker': 0.0006, + 'maker': 0.0002, 'precision': { 'price': 8, 'amount': 8, @@ -1023,6 +1047,8 @@ def get_markets(): 'spot': False, 'type': 'swap', 'contractSize': 0.01, + 'taker': 0.0006, + 'maker': 0.0002, 'precision': { 'amount': 8, 'price': 8 @@ -1098,7 +1124,6 @@ def get_markets(): 'swap': True, 'futures': False, 'option': False, - 'derivative': True, 'contract': True, 'linear': True, 'inverse': False, diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index 1c377c70a..00a2369bb 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -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._leverage_brackets = { 'BNB/BUSD': [ - [0.0, 0.025], # lev = 40.0 - [100000.0, 0.05], # lev = 20.0 - [500000.0, 0.1], # lev = 10.0 - [1000000.0, 0.15], # lev = 6.666666666666667 - [2000000.0, 0.25], # lev = 4.0 - [5000000.0, 0.5], # lev = 2.0 + [0.0, 0.025, 0.0], # lev = 40.0 + [100000.0, 0.05, 2500.0], # lev = 20.0 + [500000.0, 0.1, 27500.0], # lev = 10.0 + [1000000.0, 0.15, 77500.0], # lev = 6.666666666666667 + [2000000.0, 0.25, 277500.0], # lev = 4.0 + [5000000.0, 0.5, 1527500.0], # lev = 2.0 ], 'BNB/USDT': [ - [0.0, 0.0065], # lev = 153.84615384615384 - [10000.0, 0.01], # lev = 100.0 - [50000.0, 0.02], # lev = 50.0 - [250000.0, 0.05], # lev = 20.0 - [1000000.0, 0.1], # lev = 10.0 - [2000000.0, 0.125], # lev = 8.0 - [5000000.0, 0.15], # lev = 6.666666666666667 - [10000000.0, 0.25], # lev = 4.0 + [0.0, 0.0065, 0.0], # lev = 153.84615384615384 + [10000.0, 0.01, 35.0], # lev = 100.0 + [50000.0, 0.02, 535.0], # lev = 50.0 + [250000.0, 0.05, 8035.0], # lev = 20.0 + [1000000.0, 0.1, 58035.0], # lev = 10.0 + [2000000.0, 0.125, 108035.0], # lev = 8.0 + [5000000.0, 0.15, 233035.0], # lev = 6.666666666666667 + [10000000.0, 0.25, 1233035.0], # lev = 4.0 ], 'BTC/USDT': [ - [0.0, 0.004], # lev = 250.0 - [50000.0, 0.005], # lev = 200.0 - [250000.0, 0.01], # lev = 100.0 - [1000000.0, 0.025], # lev = 40.0 - [5000000.0, 0.05], # lev = 20.0 - [20000000.0, 0.1], # lev = 10.0 - [50000000.0, 0.125], # lev = 8.0 - [100000000.0, 0.15], # lev = 6.666666666666667 - [200000000.0, 0.25], # lev = 4.0 - [300000000.0, 0.5], # lev = 2.0 - ], + [0.0, 0.004, 0.0], # lev = 250.0 + [50000.0, 0.005, 50.0], # lev = 200.0 + [250000.0, 0.01, 1300.0], # lev = 100.0 + [1000000.0, 0.025, 16300.0], # lev = 40.0 + [5000000.0, 0.05, 141300.0], # lev = 20.0 + [20000000.0, 0.1, 1141300.0], # lev = 10.0 + [50000000.0, 0.125, 2391300.0], # lev = 8.0 + [100000000.0, 0.15, 4891300.0], # lev = 6.666666666666667 + [200000000.0, 0.25, 24891300.0], # lev = 4.0 + [300000000.0, 0.5, 99891300.0], # lev = 2.0 + ] } 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() assert exchange._leverage_brackets == { - 'ADA/BUSD': [[0.0, 0.025], - [100000.0, 0.05], - [500000.0, 0.1], - [1000000.0, 0.15], - [2000000.0, 0.25], - [5000000.0, 0.5]], - 'BTC/USDT': [[0.0, 0.004], - [50000.0, 0.005], - [250000.0, 0.01], - [1000000.0, 0.025], - [5000000.0, 0.05], - [20000000.0, 0.1], - [50000000.0, 0.125], - [100000000.0, 0.15], - [200000000.0, 0.25], - [300000000.0, 0.5]], - "ZEC/USDT": [[0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5]], + 'ADA/BUSD': [[0.0, 0.025, 0.0], + [100000.0, 0.05, 2500.0], + [500000.0, 0.1, 27500.0], + [1000000.0, 0.15, 77499.99999999999], + [2000000.0, 0.25, 277500.0], + [5000000.0, 0.5, 1527500.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.000000000002], + [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]], + "ZEC/USDT": [[0.0, 0.01, 0.0], + [5000.0, 0.025, 75.0], + [25000.0, 0.05, 700.0], + [100000.0, 0.1, 5700.0], + [250000.0, 0.125, 11949.999999999998], + [1000000.0, 0.5, 386950.0]] } api_mock = MagicMock() @@ -288,37 +288,37 @@ def test_fill_leverage_brackets_binance_dryrun(default_conf, mocker): leverage_brackets = { "1000SHIB/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] + [0.0, 0.01, 0.0], + [5000.0, 0.025, 75.0], + [25000.0, 0.05, 700.0], + [100000.0, 0.1, 5700.0], + [250000.0, 0.125, 11949.999999999998], + [1000000.0, 0.5, 386950.0], ], "1INCH/USDT": [ - [0.0, 0.012], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] + [0.0, 0.012, 0.0], + [5000.0, 0.025, 65.0], + [25000.0, 0.05, 690.0], + [100000.0, 0.1, 5690.0], + [250000.0, 0.125, 11939.999999999998], + [1000000.0, 0.5, 386940.0], ], "AAVE/USDT": [ - [0.0, 0.01], - [50000.0, 0.02], - [250000.0, 0.05], - [1000000.0, 0.1], - [2000000.0, 0.125], - [5000000.0, 0.1665], - [10000000.0, 0.25] + [0.0, 0.01, 0.0], + [50000.0, 0.02, 500.0], + [250000.0, 0.05, 8000.000000000001], + [1000000.0, 0.1, 58000.0], + [2000000.0, 0.125, 107999.99999999999], + [5000000.0, 0.1665, 315500.00000000006], + [10000000.0, 0.25, 1150500.0], ], "ADA/BUSD": [ - [0.0, 0.025], - [100000.0, 0.05], - [500000.0, 0.1], - [1000000.0, 0.15], - [2000000.0, 0.25], - [5000000.0, 0.5] + [0.0, 0.025, 0.0], + [100000.0, 0.05, 2500.0], + [500000.0, 0.1, 27500.0], + [1000000.0, 0.15, 77499.99999999999], + [2000000.0, 0.25, 277500.0], + [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 exchange = get_patched_exchange(mocker, default_conf, id="binance") 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) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 5836aa4e4..ba96e45f3 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -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!! 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, @@ -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.9999, 4, 0.001, 1, 2.999, 'spot'), (2.9909, 4, 0.001, 1, 2.990, 'spot'), - (2.9909, 4, 0.005, 0.01, 299.09, 'futures'), - (2.9999, 4, 0.005, 10, 0.295, 'futures'), + (2.9909, 4, 0.005, 0.01, 2.99, 'futures'), + (2.9999, 4, 0.005, 10, 2.995, 'futures'), ]) def test_amount_to_precision( 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.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.FUTURES, Collateral.CROSS, True), - ("binance", TradingMode.FUTURES, Collateral.ISOLATED, True), ("kraken", TradingMode.MARGIN, Collateral.CROSS, True), ("kraken", TradingMode.FUTURES, Collateral.CROSS, True), ("ftx", TradingMode.MARGIN, Collateral.CROSS, True), ("ftx", TradingMode.FUTURES, Collateral.CROSS, True), ("gateio", TradingMode.MARGIN, 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.FUTURES, Collateral.CROSS, False), - # ("binance", TradingMode.FUTURES, Collateral.ISOLATED, False), # ("kraken", TradingMode.MARGIN, Collateral.CROSS, False), # ("kraken", TradingMode.FUTURES, Collateral.CROSS, False), # ("ftx", TradingMode.MARGIN, Collateral.CROSS, False), # ("ftx", TradingMode.FUTURES, Collateral.CROSS, False), # ("gateio", TradingMode.MARGIN, Collateral.CROSS, False), # ("gateio", TradingMode.FUTURES, Collateral.CROSS, False), - # ("gateio", TradingMode.FUTURES, Collateral.ISOLATED, False), ]) def test_validate_trading_mode_and_collateral( default_conf, @@ -3582,6 +3593,68 @@ def test_calculate_funding_fees( ) == 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', [ ('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), @@ -3622,41 +3695,41 @@ def test__fetch_and_calculate_funding_fees( amount, expected_fees ): - ''' - nominal_value = mark_price * size - funding_fee = nominal_value * funding_rate - size: 30 - 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: 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: 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: 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: 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: 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: 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 + """ + nominal_value = mark_price * size + funding_fee = nominal_value * funding_rate + size: 30 + 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: 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: 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: 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: 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: 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: 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 - size: 50 - 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: 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: 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: 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: 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: 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: 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 - ''' + size: 50 + 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: 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: 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: 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: 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: 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: 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 + """ 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') funding_rate_history = { @@ -3909,3 +3982,69 @@ def test__amount_to_contracts( assert result_size == param_size result_amount = exchange._contracts_to_amount(pair, param_size) 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) diff --git a/tests/exchange/test_gateio.py b/tests/exchange/test_gateio.py index 6f7862909..a4d91c35c 100644 --- a/tests/exchange/test_gateio.py +++ b/tests/exchange/test_gateio.py @@ -1,8 +1,11 @@ +from unittest.mock import MagicMock, PropertyMock + import pytest from freqtrade.exceptions import OperationalException from freqtrade.exchange import Gateio from freqtrade.resolvers.exchange_resolver import ExchangeResolver +from tests.conftest import get_patched_exchange 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, match=r'Exchange .* does not support market orders.'): 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) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 373ffb215..a2a3bebed 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -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] -@pytest.mark.parametrize("trading_mode", [ - 'spot', - # TODO-lev: Enable other modes - # 'margin', 'futures' -] -) -@pytest.mark.parametrize("is_short", [False, True]) +@pytest.mark.parametrize("is_short,trading_mode,exchange_name,margin_mode,liq_price", [ + (False, 'spot', 'binance', None, None), + (True, 'spot', 'binance', None, None), + (False, 'spot', 'gateio', None, None), + (True, 'spot', 'gateio', None, None), + (False, 'spot', 'okex', None, None), + (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, - 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)] order = limit_order[enter_side(is_short)] default_conf_usdt['trading_mode'] = trading_mode - leverage = 1.0 if trading_mode == 'spot' else 3.0 - default_conf_usdt['collateral'] = 'cross' + leverage = 1.0 if trading_mode == 'spot' else 5.0 + 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_exchange(mocker) + patch_exchange(mocker, id=exchange_name) freqtrade = FreqtradeBot(default_conf_usdt) freqtrade.strategy.confirm_trade_entry = MagicMock(return_value=False) 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_fee=fee, get_funding_fees=MagicMock(return_value=0), + name=exchange_name ) pair = 'ETH/USDT' @@ -886,14 +916,21 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order, assert trade.open_rate_requested == 10 # 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['id'] = '5568' freqtrade.strategy.custom_entry_price = lambda **kwargs: "string price" assert freqtrade.execute_entry(pair, stake_amount, is_short=is_short) 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 assert trade assert trade.open_rate_requested == 10 + assert trade.isolated_liq == liq_price @pytest.mark.parametrize("is_short", [False, True]) @@ -4794,23 +4831,23 @@ def test_update_funding_fees( limit_order_open, schedule_off ): - ''' + """ nominal_value = mark_price * size funding_fee = nominal_value * funding_rate size = 123 - "LTC/BTC" + "LTC/USDT" 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 - "ETH/BTC" + "ETH/USDT" 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 - "ETC/BTC" + "ETC/USDT" 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 - "XRP/BTC" + "XRP/USDT" 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 - ''' + """ # SETUP 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 # But should be kept in the test to ensure we're filtering correctly. funding_rates = { - "LTC/BTC": + "LTC/USDT": DataFrame([ [date_midnight, 0.00032583, 0, 0, 0, 0], [date_eight, 0.00024472, 0, 0, 0, 0], [date_sixteen, 0.00024472, 0, 0, 0, 0], ], columns=columns), - "ETH/BTC": + "ETH/USDT": DataFrame([ [date_midnight, 0.0001, 0, 0, 0, 0], [date_eight, 0.0001, 0, 0, 0, 0], [date_sixteen, 0.0001, 0, 0, 0, 0], ], columns=columns), - "XRP/BTC": + "XRP/USDT": DataFrame([ [date_midnight, 0.00049426, 0, 0, 0, 0], [date_eight, 0.00032715, 0, 0, 0, 0], @@ -4852,19 +4889,19 @@ def test_update_funding_fees( } mark_prices = { - "LTC/BTC": + "LTC/USDT": DataFrame([ [date_midnight, 3.3, 0, 0, 0, 0], [date_eight, 3.2, 0, 0, 0, 0], [date_sixteen, 3.2, 0, 0, 0, 0], ], columns=columns), - "ETH/BTC": + "ETH/USDT": DataFrame([ [date_midnight, 2.4, 0, 0, 0, 0], [date_eight, 2.5, 0, 0, 0, 0], [date_sixteen, 2.5, 0, 0, 0, 0], ], columns=columns), - "XRP/BTC": + "XRP/USDT": DataFrame([ [date_midnight, 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) # initial funding fees, - freqtrade.execute_entry('ETH/BTC', 123, is_short=is_short) - freqtrade.execute_entry('LTC/BTC', 2.0, is_short=is_short) - freqtrade.execute_entry('XRP/BTC', 123, is_short=is_short) + freqtrade.execute_entry('ETH/USDT', 123, is_short=is_short) + freqtrade.execute_entry('LTC/USDT', 2.0, is_short=is_short) + freqtrade.execute_entry('XRP/USDT', 123, is_short=is_short) multipl = 1 if is_short else -1 trades = Trade.get_open_trades() assert len(trades) == 3 diff --git a/tests/test_persistence.py b/tests/test_persistence.py index e28e3b2ed..5df3fc2bc 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -148,7 +148,7 @@ def test_set_stop_loss_isolated_liq(fee): trade.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.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.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.stop_loss == 0.08 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.stop_loss == 0.07 assert trade.initial_stop_loss == 0.09