| @@ -15,3 +15,7 @@ For longs, the currency which pays the interest fee for the `borrowed` will alre | ||||
|     Rollover fee = P (borrowed money) * R (quat_hourly_interest) * ceiling(T/4) (in hours) | ||||
|     I (interest) = Opening fee + Rollover fee | ||||
|     [source](https://support.kraken.com/hc/en-us/articles/206161568-What-are-the-fees-for-margin-trading-) | ||||
|  | ||||
| # TODO-lev: Mention that says you can't run 2 bots on the same account with leverage, | ||||
|  | ||||
| #TODO-lev: Create a huge risk disclaimer | ||||
| @@ -20,4 +20,7 @@ class Bibox(Exchange): | ||||
|  | ||||
|     # fetchCurrencies API point requires authentication for Bibox, | ||||
|     # so switch it off for Freqtrade load_markets() | ||||
|     _ccxt_config: Dict = {"has": {"fetchCurrencies": False}} | ||||
|     @property | ||||
|     def _ccxt_config(self) -> Dict: | ||||
|         # Parameters to add directly to ccxt sync/async initialization. | ||||
|         return {"has": {"fetchCurrencies": False}} | ||||
|   | ||||
| @@ -1,10 +1,13 @@ | ||||
| """ Binance exchange subclass """ | ||||
| import json | ||||
| import logging | ||||
| from typing import Dict, List | ||||
| from pathlib import Path | ||||
| from typing import Dict, List, Optional, Tuple | ||||
|  | ||||
| import arrow | ||||
| import ccxt | ||||
|  | ||||
| from freqtrade.enums import Collateral, TradingMode | ||||
| from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException, | ||||
|                                   OperationalException, TemporaryError) | ||||
| from freqtrade.exchange import Exchange | ||||
| @@ -26,36 +29,74 @@ class Binance(Exchange): | ||||
|         "l2_limit_range": [5, 10, 20, 50, 100, 500, 1000], | ||||
|     } | ||||
|  | ||||
|     def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool: | ||||
|     _supported_trading_mode_collateral_pairs: List[Tuple[TradingMode, Collateral]] = [ | ||||
|         # TradingMode.SPOT always supported and not required in this list | ||||
|         # (TradingMode.MARGIN, Collateral.CROSS),  # TODO-lev: Uncomment once supported | ||||
|         # (TradingMode.FUTURES, Collateral.CROSS),  # TODO-lev: Uncomment once supported | ||||
|         # (TradingMode.FUTURES, Collateral.ISOLATED) # TODO-lev: Uncomment once supported | ||||
|     ] | ||||
|  | ||||
|     @property | ||||
|     def _ccxt_config(self) -> Dict: | ||||
|         # Parameters to add directly to ccxt sync/async initialization. | ||||
|         if self.trading_mode == TradingMode.MARGIN: | ||||
|             return { | ||||
|                 "options": { | ||||
|                     "defaultType": "margin" | ||||
|                 } | ||||
|             } | ||||
|         elif self.trading_mode == TradingMode.FUTURES: | ||||
|             return { | ||||
|                 "options": { | ||||
|                     "defaultType": "future" | ||||
|                 } | ||||
|             } | ||||
|         else: | ||||
|             return {} | ||||
|  | ||||
|     def stoploss_adjust(self, stop_loss: float, order: Dict, side: str) -> bool: | ||||
|         """ | ||||
|         Verify stop_loss against stoploss-order value (limit or price) | ||||
|         Returns True if adjustment is necessary. | ||||
|         :param side: "buy" or "sell" | ||||
|         """ | ||||
|         return order['type'] == 'stop_loss_limit' and stop_loss > float(order['info']['stopPrice']) | ||||
|  | ||||
|         return order['type'] == 'stop_loss_limit' and ( | ||||
|             (side == "sell" and stop_loss > float(order['info']['stopPrice'])) or | ||||
|             (side == "buy" and stop_loss < float(order['info']['stopPrice'])) | ||||
|         ) | ||||
|  | ||||
|     @retrier(retries=0) | ||||
|     def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict: | ||||
|     def stoploss(self, pair: str, amount: float, stop_price: float, | ||||
|                  order_types: Dict, side: str, leverage: float) -> Dict: | ||||
|         """ | ||||
|         creates a stoploss limit order. | ||||
|         this stoploss-limit is binance-specific. | ||||
|         It may work with a limited number of other exchanges, but this has not been tested yet. | ||||
|         :param side: "buy" or "sell" | ||||
|         """ | ||||
|         # Limit price threshold: As limit price should always be below stop-price | ||||
|         limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99) | ||||
|         rate = stop_price * limit_price_pct | ||||
|         if side == "sell": | ||||
|             # TODO: Name limit_rate in other exchange subclasses | ||||
|             rate = stop_price * limit_price_pct | ||||
|         else: | ||||
|             rate = stop_price * (2 - limit_price_pct) | ||||
|  | ||||
|         ordertype = "stop_loss_limit" | ||||
|  | ||||
|         stop_price = self.price_to_precision(pair, stop_price) | ||||
|  | ||||
|         bad_stop_price = (stop_price <= rate) if side == "sell" else (stop_price >= rate) | ||||
|  | ||||
|         # Ensure rate is less than stop price | ||||
|         if stop_price <= rate: | ||||
|         if bad_stop_price: | ||||
|             raise OperationalException( | ||||
|                 'In stoploss limit order, stop price should be more than limit price') | ||||
|                 'In stoploss limit order, stop price should be better than limit price') | ||||
|  | ||||
|         if self._config['dry_run']: | ||||
|             dry_order = self.create_dry_run_order( | ||||
|                 pair, ordertype, "sell", amount, stop_price) | ||||
|                 pair, ordertype, side, amount, stop_price, leverage) | ||||
|             return dry_order | ||||
|  | ||||
|         try: | ||||
| @@ -66,7 +107,8 @@ class Binance(Exchange): | ||||
|  | ||||
|             rate = self.price_to_precision(pair, rate) | ||||
|  | ||||
|             order = self._api.create_order(symbol=pair, type=ordertype, side='sell', | ||||
|             self._lev_prep(pair, leverage) | ||||
|             order = self._api.create_order(symbol=pair, type=ordertype, side=side, | ||||
|                                            amount=amount, price=rate, params=params) | ||||
|             logger.info('stoploss limit order added for %s. ' | ||||
|                         'stop price: %s. limit: %s', pair, stop_price, rate) | ||||
| @@ -74,21 +116,96 @@ class Binance(Exchange): | ||||
|             return order | ||||
|         except ccxt.InsufficientFunds as e: | ||||
|             raise InsufficientFundsError( | ||||
|                 f'Insufficient funds to create {ordertype} sell order on market {pair}. ' | ||||
|                 f'Tried to sell amount {amount} at rate {rate}. ' | ||||
|                 f'Insufficient funds to create {ordertype} {side} order on market {pair}. ' | ||||
|                 f'Tried to {side} amount {amount} at rate {rate}. ' | ||||
|                 f'Message: {e}') from e | ||||
|         except ccxt.InvalidOrder as e: | ||||
|             # Errors: | ||||
|             # `binance Order would trigger immediately.` | ||||
|             raise InvalidOrderException( | ||||
|                 f'Could not create {ordertype} sell order on market {pair}. ' | ||||
|                 f'Tried to sell amount {amount} at rate {rate}. ' | ||||
|                 f'Could not create {ordertype} {side} order on market {pair}. ' | ||||
|                 f'Tried to {side} amount {amount} at rate {rate}. ' | ||||
|                 f'Message: {e}') from e | ||||
|         except ccxt.DDoSProtection as e: | ||||
|             raise DDosProtection(e) from e | ||||
|         except (ccxt.NetworkError, ccxt.ExchangeError) as e: | ||||
|             raise TemporaryError( | ||||
|                 f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e | ||||
|                 f'Could not place {side} order due to {e.__class__.__name__}. Message: {e}') from e | ||||
|         except ccxt.BaseError as e: | ||||
|             raise OperationalException(e) from e | ||||
|  | ||||
|     @retrier | ||||
|     def fill_leverage_brackets(self): | ||||
|         """ | ||||
|             Assigns property _leverage_brackets to a dictionary of information about the leverage | ||||
|             allowed on each pair | ||||
|         """ | ||||
|         if self.trading_mode == TradingMode.FUTURES: | ||||
|             try: | ||||
|                 if self._config['dry_run']: | ||||
|                     leverage_brackets_path = ( | ||||
|                         Path(__file__).parent / 'binance_leverage_brackets.json' | ||||
|                     ) | ||||
|                     with open(leverage_brackets_path) as json_file: | ||||
|                         leverage_brackets = json.load(json_file) | ||||
|                 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 | ||||
|                     ] | ||||
|  | ||||
|             except ccxt.DDoSProtection as e: | ||||
|                 raise DDosProtection(e) from e | ||||
|             except (ccxt.NetworkError, ccxt.ExchangeError) as e: | ||||
|                 raise TemporaryError(f'Could not fetch leverage amounts due to' | ||||
|                                      f'{e.__class__.__name__}. Message: {e}') from e | ||||
|             except ccxt.BaseError as e: | ||||
|                 raise OperationalException(e) from e | ||||
|  | ||||
|     def get_max_leverage(self, pair: Optional[str], nominal_value: Optional[float]) -> float: | ||||
|         """ | ||||
|             Returns the maximum leverage that a pair can be traded at | ||||
|             :param pair: The base/quote currency pair being traded | ||||
|             :nominal_value: The total value of the trade in quote currency (collateral + debt) | ||||
|         """ | ||||
|         pair_brackets = self._leverage_brackets[pair] | ||||
|         max_lev = 1.0 | ||||
|         for [min_amount, margin_req] in pair_brackets: | ||||
|             if nominal_value >= min_amount: | ||||
|                 max_lev = 1/margin_req | ||||
|         return max_lev | ||||
|  | ||||
|     @ retrier | ||||
|     def _set_leverage( | ||||
|         self, | ||||
|         leverage: float, | ||||
|         pair: Optional[str] = None, | ||||
|         trading_mode: Optional[TradingMode] = None | ||||
|     ): | ||||
|         """ | ||||
|             Set's the leverage before making a trade, in order to not | ||||
|             have the same leverage on every trade | ||||
|         """ | ||||
|         trading_mode = trading_mode or self.trading_mode | ||||
|  | ||||
|         if self._config['dry_run'] or trading_mode != TradingMode.FUTURES: | ||||
|             return | ||||
|  | ||||
|         try: | ||||
|             self._api.set_leverage(symbol=pair, leverage=leverage) | ||||
|         except ccxt.DDoSProtection as e: | ||||
|             raise DDosProtection(e) from e | ||||
|         except (ccxt.NetworkError, ccxt.ExchangeError) as e: | ||||
|             raise TemporaryError( | ||||
|                 f'Could not set leverage due to {e.__class__.__name__}. Message: {e}') from e | ||||
|         except ccxt.BaseError as e: | ||||
|             raise OperationalException(e) from e | ||||
|  | ||||
|   | ||||
							
								
								
									
										1214
									
								
								freqtrade/exchange/binance_leverage_brackets.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1214
									
								
								freqtrade/exchange/binance_leverage_brackets.json
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -22,6 +22,7 @@ from pandas import DataFrame | ||||
| from freqtrade.constants import (DEFAULT_AMOUNT_RESERVE_PERCENT, NON_OPEN_EXCHANGE_STATES, | ||||
|                                  ListPairsWithTimeframes) | ||||
| from freqtrade.data.converter import ohlcv_to_dataframe, trades_dict_to_list | ||||
| from freqtrade.enums import Collateral, TradingMode | ||||
| from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFundsError, | ||||
|                                   InvalidOrderException, OperationalException, PricingError, | ||||
|                                   RetryableOrderError, TemporaryError) | ||||
| @@ -48,9 +49,6 @@ class Exchange: | ||||
|  | ||||
|     _config: Dict = {} | ||||
|  | ||||
|     # Parameters to add directly to ccxt sync/async initialization. | ||||
|     _ccxt_config: Dict = {} | ||||
|  | ||||
|     # Parameters to add directly to buy/sell calls (like agreeing to trading agreement) | ||||
|     _params: Dict = {} | ||||
|  | ||||
| @@ -74,6 +72,10 @@ class Exchange: | ||||
|     } | ||||
|     _ft_has: Dict = {} | ||||
|  | ||||
|     _supported_trading_mode_collateral_pairs: List[Tuple[TradingMode, Collateral]] = [ | ||||
|         # TradingMode.SPOT always supported and not required in this list | ||||
|     ] | ||||
|  | ||||
|     def __init__(self, config: Dict[str, Any], validate: bool = True) -> None: | ||||
|         """ | ||||
|         Initializes this module with the given config, | ||||
| @@ -83,6 +85,7 @@ class Exchange: | ||||
|         self._api: ccxt.Exchange = None | ||||
|         self._api_async: ccxt_async.Exchange = None | ||||
|         self._markets: Dict = {} | ||||
|         self._leverage_brackets: Dict = {} | ||||
|  | ||||
|         self._config.update(config) | ||||
|  | ||||
| @@ -125,14 +128,25 @@ class Exchange: | ||||
|         self._trades_pagination = self._ft_has['trades_pagination'] | ||||
|         self._trades_pagination_arg = self._ft_has['trades_pagination_arg'] | ||||
|  | ||||
|         self.trading_mode: TradingMode = ( | ||||
|             TradingMode(config.get('trading_mode')) | ||||
|             if config.get('trading_mode') | ||||
|             else TradingMode.SPOT | ||||
|         ) | ||||
|         self.collateral: Optional[Collateral] = ( | ||||
|             Collateral(config.get('collateral')) | ||||
|             if config.get('collateral') | ||||
|             else None | ||||
|         ) | ||||
|  | ||||
|         # Initialize ccxt objects | ||||
|         ccxt_config = self._ccxt_config.copy() | ||||
|         ccxt_config = self._ccxt_config | ||||
|         ccxt_config = deep_merge_dicts(exchange_config.get('ccxt_config', {}), ccxt_config) | ||||
|         ccxt_config = deep_merge_dicts(exchange_config.get('ccxt_sync_config', {}), ccxt_config) | ||||
|  | ||||
|         self._api = self._init_ccxt(exchange_config, ccxt_kwargs=ccxt_config) | ||||
|  | ||||
|         ccxt_async_config = self._ccxt_config.copy() | ||||
|         ccxt_async_config = self._ccxt_config | ||||
|         ccxt_async_config = deep_merge_dicts(exchange_config.get('ccxt_config', {}), | ||||
|                                              ccxt_async_config) | ||||
|         ccxt_async_config = deep_merge_dicts(exchange_config.get('ccxt_async_config', {}), | ||||
| @@ -140,6 +154,9 @@ class Exchange: | ||||
|         self._api_async = self._init_ccxt( | ||||
|             exchange_config, ccxt_async, ccxt_kwargs=ccxt_async_config) | ||||
|  | ||||
|         if self.trading_mode != TradingMode.SPOT: | ||||
|             self.fill_leverage_brackets() | ||||
|  | ||||
|         logger.info('Using Exchange "%s"', self.name) | ||||
|  | ||||
|         if validate: | ||||
| @@ -157,7 +174,7 @@ class Exchange: | ||||
|             self.validate_order_time_in_force(config.get('order_time_in_force', {})) | ||||
|             self.validate_required_startup_candles(config.get('startup_candle_count', 0), | ||||
|                                                    config.get('timeframe', '')) | ||||
|  | ||||
|             self.validate_trading_mode_and_collateral(self.trading_mode, self.collateral) | ||||
|         # Converts the interval provided in minutes in config to seconds | ||||
|         self.markets_refresh_interval: int = exchange_config.get( | ||||
|             "markets_refresh_interval", 60) * 60 | ||||
| @@ -190,6 +207,7 @@ class Exchange: | ||||
|             'secret': exchange_config.get('secret'), | ||||
|             'password': exchange_config.get('password'), | ||||
|             'uid': exchange_config.get('uid', ''), | ||||
|             # 'options': exchange_config.get('options', {}) | ||||
|         } | ||||
|         if ccxt_kwargs: | ||||
|             logger.info('Applying additional ccxt config: %s', ccxt_kwargs) | ||||
| @@ -210,6 +228,11 @@ class Exchange: | ||||
|  | ||||
|         return api | ||||
|  | ||||
|     @property | ||||
|     def _ccxt_config(self) -> Dict: | ||||
|         # Parameters to add directly to ccxt sync/async initialization. | ||||
|         return {} | ||||
|  | ||||
|     @property | ||||
|     def name(self) -> str: | ||||
|         """exchange Name (from ccxt)""" | ||||
| @@ -355,6 +378,7 @@ class Exchange: | ||||
|             # Also reload async markets to avoid issues with newly listed pairs | ||||
|             self._load_async_markets(reload=True) | ||||
|             self._last_markets_refresh = arrow.utcnow().int_timestamp | ||||
|             self.fill_leverage_brackets() | ||||
|         except ccxt.BaseError: | ||||
|             logger.exception("Could not reload markets.") | ||||
|  | ||||
| @@ -370,7 +394,7 @@ class Exchange: | ||||
|             raise OperationalException( | ||||
|                 'Could not load markets, therefore cannot start. ' | ||||
|                 'Please investigate the above error for more details.' | ||||
|                 ) | ||||
|             ) | ||||
|         quote_currencies = self.get_quote_currencies() | ||||
|         if stake_currency not in quote_currencies: | ||||
|             raise OperationalException( | ||||
| @@ -482,6 +506,25 @@ class Exchange: | ||||
|                 f"This strategy requires {startup_candles} candles to start. " | ||||
|                 f"{self.name} only provides {candle_limit} for {timeframe}.") | ||||
|  | ||||
|     def validate_trading_mode_and_collateral( | ||||
|         self, | ||||
|         trading_mode: TradingMode, | ||||
|         collateral: Optional[Collateral]  # Only None when trading_mode = TradingMode.SPOT | ||||
|     ): | ||||
|         """ | ||||
|             Checks if freqtrade can perform trades using the configured | ||||
|             trading mode(Margin, Futures) and Collateral(Cross, Isolated) | ||||
|             Throws OperationalException: | ||||
|                 If the trading_mode/collateral type are not supported by freqtrade on this exchange | ||||
|         """ | ||||
|         if trading_mode != TradingMode.SPOT and ( | ||||
|             (trading_mode, collateral) not in self._supported_trading_mode_collateral_pairs | ||||
|         ): | ||||
|             collateral_value = collateral and collateral.value | ||||
|             raise OperationalException( | ||||
|                 f"Freqtrade does not support {collateral_value} {trading_mode.value} on {self.name}" | ||||
|             ) | ||||
|  | ||||
|     def exchange_has(self, endpoint: str) -> bool: | ||||
|         """ | ||||
|         Checks if exchange implements a specific API endpoint. | ||||
| @@ -541,8 +584,8 @@ class Exchange: | ||||
|         else: | ||||
|             return 1 / pow(10, precision) | ||||
|  | ||||
|     def get_min_pair_stake_amount(self, pair: str, price: float, | ||||
|                                   stoploss: float) -> Optional[float]: | ||||
|     def get_min_pair_stake_amount(self, pair: str, price: float, stoploss: float, | ||||
|                                   leverage: Optional[float] = 1.0) -> Optional[float]: | ||||
|         try: | ||||
|             market = self.markets[pair] | ||||
|         except KeyError: | ||||
| @@ -576,12 +619,24 @@ class Exchange: | ||||
|         # The value returned should satisfy both limits: for amount (base currency) and | ||||
|         # for cost (quote, stake currency), so max() is used here. | ||||
|         # See also #2575 at github. | ||||
|         return max(min_stake_amounts) * amount_reserve_percent | ||||
|         return self._get_stake_amount_considering_leverage( | ||||
|             max(min_stake_amounts) * amount_reserve_percent, | ||||
|             leverage or 1.0 | ||||
|         ) | ||||
|  | ||||
|     def _get_stake_amount_considering_leverage(self, stake_amount: float, leverage: float): | ||||
|         """ | ||||
|         Takes the minimum stake amount for a pair with no leverage and returns the minimum | ||||
|         stake amount when leverage is considered | ||||
|         :param stake_amount: The stake amount for a pair before leverage is considered | ||||
|         :param leverage: The amount of leverage being used on the current trade | ||||
|         """ | ||||
|         return stake_amount / leverage | ||||
|  | ||||
|     # Dry-run methods | ||||
|  | ||||
|     def create_dry_run_order(self, pair: str, ordertype: str, side: str, amount: float, | ||||
|                              rate: float, params: Dict = {}) -> Dict[str, Any]: | ||||
|                              rate: float, leverage: float, params: Dict = {}) -> Dict[str, Any]: | ||||
|         order_id = f'dry_run_{side}_{datetime.now().timestamp()}' | ||||
|         _amount = self.amount_to_precision(pair, amount) | ||||
|         dry_order: Dict[str, Any] = { | ||||
| @@ -598,7 +653,8 @@ class Exchange: | ||||
|             'timestamp': arrow.utcnow().int_timestamp * 1000, | ||||
|             'status': "closed" if ordertype == "market" else "open", | ||||
|             'fee': None, | ||||
|             'info': {} | ||||
|             'info': {}, | ||||
|             'leverage': leverage | ||||
|         } | ||||
|         if dry_order["type"] in ["stop_loss_limit", "stop-loss-limit"]: | ||||
|             dry_order["info"] = {"stopPrice": dry_order["price"]} | ||||
| @@ -608,7 +664,7 @@ class Exchange: | ||||
|             average = self.get_dry_market_fill_price(pair, side, amount, rate) | ||||
|             dry_order.update({ | ||||
|                 'average': average, | ||||
|                 'cost': dry_order['amount'] * average, | ||||
|                 'cost': (dry_order['amount'] * average) / leverage | ||||
|             }) | ||||
|             dry_order = self.add_dry_order_fee(pair, dry_order) | ||||
|  | ||||
| @@ -716,17 +772,26 @@ class Exchange: | ||||
|  | ||||
|     # Order handling | ||||
|  | ||||
|     def create_order(self, pair: str, ordertype: str, side: str, amount: float, | ||||
|                      rate: float, time_in_force: str = 'gtc') -> Dict: | ||||
|  | ||||
|         if self._config['dry_run']: | ||||
|             dry_order = self.create_dry_run_order(pair, ordertype, side, amount, rate) | ||||
|             return dry_order | ||||
|     def _lev_prep(self, pair: str, leverage: float): | ||||
|         if self.trading_mode != TradingMode.SPOT: | ||||
|             self.set_margin_mode(pair, self.collateral) | ||||
|             self._set_leverage(leverage, pair) | ||||
|  | ||||
|     def _get_params(self, ordertype: str, leverage: float, time_in_force: str = 'gtc') -> Dict: | ||||
|         params = self._params.copy() | ||||
|         if time_in_force != 'gtc' and ordertype != 'market': | ||||
|             param = self._ft_has.get('time_in_force_parameter', '') | ||||
|             params.update({param: time_in_force}) | ||||
|         return params | ||||
|  | ||||
|     def create_order(self, pair: str, ordertype: str, side: str, amount: float, | ||||
|                      rate: float, leverage: float = 1.0, time_in_force: str = 'gtc') -> Dict: | ||||
|         # TODO-lev: remove default for leverage | ||||
|         if self._config['dry_run']: | ||||
|             dry_order = self.create_dry_run_order(pair, ordertype, side, amount, rate, leverage) | ||||
|             return dry_order | ||||
|  | ||||
|         params = self._get_params(ordertype, leverage, time_in_force) | ||||
|  | ||||
|         try: | ||||
|             # Set the precision for amount and price(rate) as accepted by the exchange | ||||
| @@ -735,6 +800,7 @@ class Exchange: | ||||
|                            or self._api.options.get("createMarketBuyOrderRequiresPrice", False)) | ||||
|             rate_for_order = self.price_to_precision(pair, rate) if needs_price else None | ||||
|  | ||||
|             self._lev_prep(pair, leverage) | ||||
|             order = self._api.create_order(pair, ordertype, side, | ||||
|                                            amount, rate_for_order, params) | ||||
|             self._log_exchange_response('create_order', order) | ||||
| @@ -758,14 +824,15 @@ class Exchange: | ||||
|         except ccxt.BaseError as e: | ||||
|             raise OperationalException(e) from e | ||||
|  | ||||
|     def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool: | ||||
|     def stoploss_adjust(self, stop_loss: float, order: Dict, side: str) -> bool: | ||||
|         """ | ||||
|         Verify stop_loss against stoploss-order value (limit or price) | ||||
|         Returns True if adjustment is necessary. | ||||
|         """ | ||||
|         raise OperationalException(f"stoploss is not implemented for {self.name}.") | ||||
|  | ||||
|     def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict: | ||||
|     def stoploss(self, pair: str, amount: float, stop_price: float, | ||||
|                  order_types: Dict, side: str, leverage: float) -> Dict: | ||||
|         """ | ||||
|         creates a stoploss order. | ||||
|         The precise ordertype is determined by the order_types dict or exchange default. | ||||
| @@ -1528,6 +1595,69 @@ class Exchange: | ||||
|             self._async_get_trade_history(pair=pair, since=since, | ||||
|                                           until=until, from_id=from_id)) | ||||
|  | ||||
|     def fill_leverage_brackets(self): | ||||
|         """ | ||||
|             # TODO-lev: Should maybe be renamed, leverage_brackets might not be accurate for kraken | ||||
|             Assigns property _leverage_brackets to a dictionary of information about the leverage | ||||
|             allowed on each pair | ||||
|         """ | ||||
|         return | ||||
|  | ||||
|     def get_max_leverage(self, pair: Optional[str], nominal_value: Optional[float]) -> float: | ||||
|         """ | ||||
|             Returns the maximum leverage that a pair can be traded at | ||||
|             :param pair: The base/quote currency pair being traded | ||||
|             :nominal_value: The total value of the trade in quote currency (collateral + debt) | ||||
|         """ | ||||
|         return 1.0 | ||||
|  | ||||
|     @retrier | ||||
|     def _set_leverage( | ||||
|         self, | ||||
|         leverage: float, | ||||
|         pair: Optional[str] = None, | ||||
|         trading_mode: Optional[TradingMode] = None | ||||
|     ): | ||||
|         """ | ||||
|             Set's the leverage before making a trade, in order to not | ||||
|             have the same leverage on every trade | ||||
|         """ | ||||
|         # TODO-lev: Make a documentation page that says you can't run 2 bots | ||||
|         # TODO-lev: on the same account with leverage | ||||
|         if self._config['dry_run'] or not self.exchange_has("setLeverage"): | ||||
|             # Some exchanges only support one collateral type | ||||
|             return | ||||
|  | ||||
|         try: | ||||
|             self._api.set_leverage(symbol=pair, leverage=leverage) | ||||
|         except ccxt.DDoSProtection as e: | ||||
|             raise DDosProtection(e) from e | ||||
|         except (ccxt.NetworkError, ccxt.ExchangeError) as e: | ||||
|             raise TemporaryError( | ||||
|                 f'Could not set leverage due to {e.__class__.__name__}. Message: {e}') from e | ||||
|         except ccxt.BaseError as e: | ||||
|             raise OperationalException(e) from e | ||||
|  | ||||
|     @retrier | ||||
|     def set_margin_mode(self, pair: str, collateral: Collateral, params: dict = {}): | ||||
|         ''' | ||||
|             Set's the margin mode on the exchange to cross or isolated for a specific pair | ||||
|             :param symbol: base/quote currency pair (e.g. "ADA/USDT") | ||||
|         ''' | ||||
|         if self._config['dry_run'] or not self.exchange_has("setMarginMode"): | ||||
|             # Some exchanges only support one collateral type | ||||
|             return | ||||
|  | ||||
|         try: | ||||
|             self._api.set_margin_mode(pair, collateral.value, params) | ||||
|         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 is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = None) -> bool: | ||||
|     return exchange_name in ccxt_exchanges(ccxt_module) | ||||
|   | ||||
| @@ -1,9 +1,10 @@ | ||||
| """ FTX exchange subclass """ | ||||
| import logging | ||||
| from typing import Any, Dict | ||||
| from typing import Any, Dict, List, Optional, Tuple | ||||
|  | ||||
| import ccxt | ||||
|  | ||||
| from freqtrade.enums import Collateral, TradingMode | ||||
| from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException, | ||||
|                                   OperationalException, TemporaryError) | ||||
| from freqtrade.exchange import Exchange | ||||
| @@ -21,6 +22,12 @@ class Ftx(Exchange): | ||||
|         "ohlcv_candle_limit": 1500, | ||||
|     } | ||||
|  | ||||
|     _supported_trading_mode_collateral_pairs: List[Tuple[TradingMode, Collateral]] = [ | ||||
|         # TradingMode.SPOT always supported and not required in this list | ||||
|         # (TradingMode.MARGIN, Collateral.CROSS),  # TODO-lev: Uncomment once supported | ||||
|         # (TradingMode.FUTURES, Collateral.CROSS)  # TODO-lev: Uncomment once supported | ||||
|     ] | ||||
|  | ||||
|     def market_is_tradable(self, market: Dict[str, Any]) -> bool: | ||||
|         """ | ||||
|         Check if the market symbol is tradable by Freqtrade. | ||||
| @@ -31,15 +38,19 @@ class Ftx(Exchange): | ||||
|         return (parent_check and | ||||
|                 market.get('spot', False) is True) | ||||
|  | ||||
|     def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool: | ||||
|     def stoploss_adjust(self, stop_loss: float, order: Dict, side: str) -> bool: | ||||
|         """ | ||||
|         Verify stop_loss against stoploss-order value (limit or price) | ||||
|         Returns True if adjustment is necessary. | ||||
|         """ | ||||
|         return order['type'] == 'stop' and stop_loss > float(order['price']) | ||||
|         return order['type'] == 'stop' and ( | ||||
|             side == "sell" and stop_loss > float(order['price']) or | ||||
|             side == "buy" and stop_loss < float(order['price']) | ||||
|         ) | ||||
|  | ||||
|     @retrier(retries=0) | ||||
|     def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict: | ||||
|     def stoploss(self, pair: str, amount: float, stop_price: float, | ||||
|                  order_types: Dict, side: str, leverage: float) -> Dict: | ||||
|         """ | ||||
|         Creates a stoploss order. | ||||
|         depending on order_types.stoploss configuration, uses 'market' or limit order. | ||||
| @@ -47,7 +58,10 @@ class Ftx(Exchange): | ||||
|         Limit orders are defined by having orderPrice set, otherwise a market order is used. | ||||
|         """ | ||||
|         limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99) | ||||
|         limit_rate = stop_price * limit_price_pct | ||||
|         if side == "sell": | ||||
|             limit_rate = stop_price * limit_price_pct | ||||
|         else: | ||||
|             limit_rate = stop_price * (2 - limit_price_pct) | ||||
|  | ||||
|         ordertype = "stop" | ||||
|  | ||||
| @@ -55,7 +69,7 @@ class Ftx(Exchange): | ||||
|  | ||||
|         if self._config['dry_run']: | ||||
|             dry_order = self.create_dry_run_order( | ||||
|                 pair, ordertype, "sell", amount, stop_price) | ||||
|                 pair, ordertype, side, amount, stop_price, leverage) | ||||
|             return dry_order | ||||
|  | ||||
|         try: | ||||
| @@ -67,7 +81,8 @@ class Ftx(Exchange): | ||||
|             params['stopPrice'] = stop_price | ||||
|             amount = self.amount_to_precision(pair, amount) | ||||
|  | ||||
|             order = self._api.create_order(symbol=pair, type=ordertype, side='sell', | ||||
|             self._lev_prep(pair, leverage) | ||||
|             order = self._api.create_order(symbol=pair, type=ordertype, side=side, | ||||
|                                            amount=amount, params=params) | ||||
|             self._log_exchange_response('create_stoploss_order', order) | ||||
|             logger.info('stoploss order added for %s. ' | ||||
| @@ -75,19 +90,19 @@ class Ftx(Exchange): | ||||
|             return order | ||||
|         except ccxt.InsufficientFunds as e: | ||||
|             raise InsufficientFundsError( | ||||
|                 f'Insufficient funds to create {ordertype} sell order on market {pair}. ' | ||||
|                 f'Insufficient funds to create {ordertype} {side} order on market {pair}. ' | ||||
|                 f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. ' | ||||
|                 f'Message: {e}') from e | ||||
|         except ccxt.InvalidOrder as e: | ||||
|             raise InvalidOrderException( | ||||
|                 f'Could not create {ordertype} sell order on market {pair}. ' | ||||
|                 f'Could not create {ordertype} {side} order on market {pair}. ' | ||||
|                 f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. ' | ||||
|                 f'Message: {e}') from e | ||||
|         except ccxt.DDoSProtection as e: | ||||
|             raise DDosProtection(e) from e | ||||
|         except (ccxt.NetworkError, ccxt.ExchangeError) as e: | ||||
|             raise TemporaryError( | ||||
|                 f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e | ||||
|                 f'Could not place {side} order due to {e.__class__.__name__}. Message: {e}') from e | ||||
|         except ccxt.BaseError as e: | ||||
|             raise OperationalException(e) from e | ||||
|  | ||||
| @@ -152,3 +167,18 @@ class Ftx(Exchange): | ||||
|         if order['type'] == 'stop': | ||||
|             return safe_value_fallback2(order, order, 'id_stop', 'id') | ||||
|         return order['id'] | ||||
|  | ||||
|     def fill_leverage_brackets(self): | ||||
|         """ | ||||
|             FTX leverage is static across the account, and doesn't change from pair to pair, | ||||
|             so _leverage_brackets doesn't need to be set | ||||
|         """ | ||||
|         return | ||||
|  | ||||
|     def get_max_leverage(self, pair: Optional[str], nominal_value: Optional[float]) -> float: | ||||
|         """ | ||||
|             Returns the maximum leverage that a pair can be traded at, which is always 20 on ftx | ||||
|             :param pair: Here for super method, not used on FTX | ||||
|             :nominal_value: Here for super method, not used on FTX | ||||
|         """ | ||||
|         return 20.0 | ||||
|   | ||||
| @@ -1,9 +1,10 @@ | ||||
| """ Kraken exchange subclass """ | ||||
| import logging | ||||
| from typing import Any, Dict | ||||
| from typing import Any, Dict, List, Optional, Tuple | ||||
|  | ||||
| import ccxt | ||||
|  | ||||
| from freqtrade.enums import Collateral, TradingMode | ||||
| from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException, | ||||
|                                   OperationalException, TemporaryError) | ||||
| from freqtrade.exchange import Exchange | ||||
| @@ -23,6 +24,12 @@ class Kraken(Exchange): | ||||
|         "trades_pagination_arg": "since", | ||||
|     } | ||||
|  | ||||
|     _supported_trading_mode_collateral_pairs: List[Tuple[TradingMode, Collateral]] = [ | ||||
|         # TradingMode.SPOT always supported and not required in this list | ||||
|         # (TradingMode.MARGIN, Collateral.CROSS),  # TODO-lev: Uncomment once supported | ||||
|         # (TradingMode.FUTURES, Collateral.CROSS)  # TODO-lev: No CCXT support | ||||
|     ] | ||||
|  | ||||
|     def market_is_tradable(self, market: Dict[str, Any]) -> bool: | ||||
|         """ | ||||
|         Check if the market symbol is tradable by Freqtrade. | ||||
| @@ -67,16 +74,19 @@ class Kraken(Exchange): | ||||
|         except ccxt.BaseError as e: | ||||
|             raise OperationalException(e) from e | ||||
|  | ||||
|     def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool: | ||||
|     def stoploss_adjust(self, stop_loss: float, order: Dict, side: str) -> bool: | ||||
|         """ | ||||
|         Verify stop_loss against stoploss-order value (limit or price) | ||||
|         Returns True if adjustment is necessary. | ||||
|         """ | ||||
|         return (order['type'] in ('stop-loss', 'stop-loss-limit') | ||||
|                 and stop_loss > float(order['price'])) | ||||
|         return (order['type'] in ('stop-loss', 'stop-loss-limit') and ( | ||||
|                 (side == "sell" and stop_loss > float(order['price'])) or | ||||
|                 (side == "buy" and stop_loss < float(order['price'])) | ||||
|                 )) | ||||
|  | ||||
|     @retrier(retries=0) | ||||
|     def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict: | ||||
|     def stoploss(self, pair: str, amount: float, stop_price: float, | ||||
|                  order_types: Dict, side: str, leverage: float) -> Dict: | ||||
|         """ | ||||
|         Creates a stoploss market order. | ||||
|         Stoploss market orders is the only stoploss type supported by kraken. | ||||
| @@ -86,7 +96,10 @@ class Kraken(Exchange): | ||||
|         if order_types.get('stoploss', 'market') == 'limit': | ||||
|             ordertype = "stop-loss-limit" | ||||
|             limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99) | ||||
|             limit_rate = stop_price * limit_price_pct | ||||
|             if side == "sell": | ||||
|                 limit_rate = stop_price * limit_price_pct | ||||
|             else: | ||||
|                 limit_rate = stop_price * (2 - limit_price_pct) | ||||
|             params['price2'] = self.price_to_precision(pair, limit_rate) | ||||
|         else: | ||||
|             ordertype = "stop-loss" | ||||
| @@ -95,13 +108,13 @@ class Kraken(Exchange): | ||||
|  | ||||
|         if self._config['dry_run']: | ||||
|             dry_order = self.create_dry_run_order( | ||||
|                 pair, ordertype, "sell", amount, stop_price) | ||||
|                 pair, ordertype, side, amount, stop_price, leverage) | ||||
|             return dry_order | ||||
|  | ||||
|         try: | ||||
|             amount = self.amount_to_precision(pair, amount) | ||||
|  | ||||
|             order = self._api.create_order(symbol=pair, type=ordertype, side='sell', | ||||
|             order = self._api.create_order(symbol=pair, type=ordertype, side=side, | ||||
|                                            amount=amount, price=stop_price, params=params) | ||||
|             self._log_exchange_response('create_stoploss_order', order) | ||||
|             logger.info('stoploss order added for %s. ' | ||||
| @@ -109,18 +122,70 @@ class Kraken(Exchange): | ||||
|             return order | ||||
|         except ccxt.InsufficientFunds as e: | ||||
|             raise InsufficientFundsError( | ||||
|                 f'Insufficient funds to create {ordertype} sell order on market {pair}. ' | ||||
|                 f'Insufficient funds to create {ordertype} {side} order on market {pair}. ' | ||||
|                 f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. ' | ||||
|                 f'Message: {e}') from e | ||||
|         except ccxt.InvalidOrder as e: | ||||
|             raise InvalidOrderException( | ||||
|                 f'Could not create {ordertype} sell order on market {pair}. ' | ||||
|                 f'Could not create {ordertype} {side} order on market {pair}. ' | ||||
|                 f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. ' | ||||
|                 f'Message: {e}') from e | ||||
|         except ccxt.DDoSProtection as e: | ||||
|             raise DDosProtection(e) from e | ||||
|         except (ccxt.NetworkError, ccxt.ExchangeError) as e: | ||||
|             raise TemporaryError( | ||||
|                 f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e | ||||
|                 f'Could not place {side} order due to {e.__class__.__name__}. Message: {e}') from e | ||||
|         except ccxt.BaseError as e: | ||||
|             raise OperationalException(e) from e | ||||
|  | ||||
|     def fill_leverage_brackets(self): | ||||
|         """ | ||||
|             Assigns property _leverage_brackets to a dictionary of information about the leverage | ||||
|             allowed on each pair | ||||
|         """ | ||||
|         leverages = {} | ||||
|  | ||||
|         for pair, market in self.markets.items(): | ||||
|             leverages[pair] = [1] | ||||
|             info = market['info'] | ||||
|             leverage_buy = info.get('leverage_buy', []) | ||||
|             leverage_sell = info.get('leverage_sell', []) | ||||
|             if len(leverage_buy) > 0 or len(leverage_sell) > 0: | ||||
|                 if leverage_buy != leverage_sell: | ||||
|                     logger.warning( | ||||
|                         f"The buy({leverage_buy}) and sell({leverage_sell}) leverage are not equal" | ||||
|                         "for {pair}. Please notify freqtrade because this has never happened before" | ||||
|                     ) | ||||
|                     if max(leverage_buy) <= max(leverage_sell): | ||||
|                         leverages[pair] += [int(lev) for lev in leverage_buy] | ||||
|                     else: | ||||
|                         leverages[pair] += [int(lev) for lev in leverage_sell] | ||||
|                 else: | ||||
|                     leverages[pair] += [int(lev) for lev in leverage_buy] | ||||
|         self._leverage_brackets = leverages | ||||
|  | ||||
|     def get_max_leverage(self, pair: Optional[str], nominal_value: Optional[float]) -> float: | ||||
|         """ | ||||
|             Returns the maximum leverage that a pair can be traded at | ||||
|             :param pair: The base/quote currency pair being traded | ||||
|             :nominal_value: Here for super class, not needed on Kraken | ||||
|         """ | ||||
|         return float(max(self._leverage_brackets[pair])) | ||||
|  | ||||
|     def _set_leverage( | ||||
|         self, | ||||
|         leverage: float, | ||||
|         pair: Optional[str] = None, | ||||
|         trading_mode: Optional[TradingMode] = None | ||||
|     ): | ||||
|         """ | ||||
|             Kraken set's the leverage as an option in the order object, so we need to | ||||
|             add it to params | ||||
|         """ | ||||
|         return | ||||
|  | ||||
|     def _get_params(self, ordertype: str, leverage: float, time_in_force: str = 'gtc') -> Dict: | ||||
|         params = super()._get_params(ordertype, leverage, time_in_force) | ||||
|         if leverage > 1.0: | ||||
|             params['leverage'] = leverage | ||||
|         return params | ||||
|   | ||||
| @@ -732,9 +732,14 @@ class FreqtradeBot(LoggingMixin): | ||||
|         :return: True if the order succeeded, and False in case of problems. | ||||
|         """ | ||||
|         try: | ||||
|             stoploss_order = self.exchange.stoploss(pair=trade.pair, amount=trade.amount, | ||||
|                                                     stop_price=stop_price, | ||||
|                                                     order_types=self.strategy.order_types) | ||||
|             stoploss_order = self.exchange.stoploss( | ||||
|                 pair=trade.pair, | ||||
|                 amount=trade.amount, | ||||
|                 stop_price=stop_price, | ||||
|                 order_types=self.strategy.order_types, | ||||
|                 side=trade.exit_side, | ||||
|                 leverage=trade.leverage | ||||
|             ) | ||||
|  | ||||
|             order_obj = Order.parse_from_ccxt_object(stoploss_order, trade.pair, 'stoploss') | ||||
|             trade.orders.append(order_obj) | ||||
| @@ -826,11 +831,11 @@ class FreqtradeBot(LoggingMixin): | ||||
|             # if trailing stoploss is enabled we check if stoploss value has changed | ||||
|             # in which case we cancel stoploss order and put another one with new | ||||
|             # value immediately | ||||
|             self.handle_trailing_stoploss_on_exchange(trade, stoploss_order) | ||||
|             self.handle_trailing_stoploss_on_exchange(trade, stoploss_order, side=trade.exit_side) | ||||
|  | ||||
|         return False | ||||
|  | ||||
|     def handle_trailing_stoploss_on_exchange(self, trade: Trade, order: dict) -> None: | ||||
|     def handle_trailing_stoploss_on_exchange(self, trade: Trade, order: dict, side: str) -> None: | ||||
|         """ | ||||
|         Check to see if stoploss on exchange should be updated | ||||
|         in case of trailing stoploss on exchange | ||||
| @@ -838,7 +843,7 @@ class FreqtradeBot(LoggingMixin): | ||||
|         :param order: Current on exchange stoploss order | ||||
|         :return: None | ||||
|         """ | ||||
|         if self.exchange.stoploss_adjust(trade.stop_loss, order): | ||||
|         if self.exchange.stoploss_adjust(trade.stop_loss, order, side): | ||||
|             # we check if the update is necessary | ||||
|             update_beat = self.strategy.order_types.get('stoploss_on_exchange_interval', 60) | ||||
|             if (datetime.utcnow() - trade.stoploss_last_update).total_seconds() >= update_beat: | ||||
|   | ||||
| @@ -18,7 +18,7 @@ from freqtrade import constants | ||||
| from freqtrade.commands import Arguments | ||||
| from freqtrade.data.converter import ohlcv_to_dataframe | ||||
| from freqtrade.edge import Edge, PairInfo | ||||
| from freqtrade.enums import RunMode | ||||
| from freqtrade.enums import Collateral, RunMode, TradingMode | ||||
| from freqtrade.exchange import Exchange | ||||
| from freqtrade.freqtradebot import FreqtradeBot | ||||
| from freqtrade.persistence import LocalTrade, Trade, init_db | ||||
| @@ -81,7 +81,13 @@ def patched_configuration_load_config_file(mocker, config) -> None: | ||||
|     ) | ||||
|  | ||||
|  | ||||
| def patch_exchange(mocker, api_mock=None, id='binance', mock_markets=True) -> None: | ||||
| def patch_exchange( | ||||
|     mocker, | ||||
|     api_mock=None, | ||||
|     id='binance', | ||||
|     mock_markets=True, | ||||
|     mock_supported_modes=True | ||||
| ) -> None: | ||||
|     mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock(return_value={})) | ||||
|     mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock()) | ||||
|     mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) | ||||
| @@ -90,10 +96,22 @@ def patch_exchange(mocker, api_mock=None, id='binance', mock_markets=True) -> No | ||||
|     mocker.patch('freqtrade.exchange.Exchange.id', PropertyMock(return_value=id)) | ||||
|     mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value=id.title())) | ||||
|     mocker.patch('freqtrade.exchange.Exchange.precisionMode', PropertyMock(return_value=2)) | ||||
|  | ||||
|     if mock_markets: | ||||
|         mocker.patch('freqtrade.exchange.Exchange.markets', | ||||
|                      PropertyMock(return_value=get_markets())) | ||||
|  | ||||
|     if mock_supported_modes: | ||||
|         mocker.patch( | ||||
|             f'freqtrade.exchange.{id.capitalize()}._supported_trading_mode_collateral_pairs', | ||||
|             PropertyMock(return_value=[ | ||||
|                 (TradingMode.MARGIN, Collateral.CROSS), | ||||
|                 (TradingMode.MARGIN, Collateral.ISOLATED), | ||||
|                 (TradingMode.FUTURES, Collateral.CROSS), | ||||
|                 (TradingMode.FUTURES, Collateral.ISOLATED) | ||||
|             ]) | ||||
|         ) | ||||
|  | ||||
|     if api_mock: | ||||
|         mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) | ||||
|     else: | ||||
| @@ -101,8 +119,8 @@ def patch_exchange(mocker, api_mock=None, id='binance', mock_markets=True) -> No | ||||
|  | ||||
|  | ||||
| def get_patched_exchange(mocker, config, api_mock=None, id='binance', | ||||
|                          mock_markets=True) -> Exchange: | ||||
|     patch_exchange(mocker, api_mock, id, mock_markets) | ||||
|                          mock_markets=True, mock_supported_modes=True) -> Exchange: | ||||
|     patch_exchange(mocker, api_mock, id, mock_markets, mock_supported_modes) | ||||
|     config['exchange']['name'] = id | ||||
|     try: | ||||
|         exchange = ExchangeResolver.load_exchange(id, config) | ||||
| @@ -442,7 +460,10 @@ def get_markets(): | ||||
|                     'max': 500000, | ||||
|                 }, | ||||
|             }, | ||||
|             'info': {}, | ||||
|             'info': { | ||||
|                 'leverage_buy': ['2'], | ||||
|                 'leverage_sell': ['2'], | ||||
|             }, | ||||
|         }, | ||||
|         'TKN/BTC': { | ||||
|             'id': 'tknbtc', | ||||
| @@ -468,7 +489,10 @@ def get_markets(): | ||||
|                     'max': 500000, | ||||
|                 }, | ||||
|             }, | ||||
|             'info': {}, | ||||
|             'info': { | ||||
|                 'leverage_buy': ['2', '3', '4', '5'], | ||||
|                 'leverage_sell': ['2', '3', '4', '5'], | ||||
|             }, | ||||
|         }, | ||||
|         'BLK/BTC': { | ||||
|             'id': 'blkbtc', | ||||
| @@ -493,7 +517,10 @@ def get_markets(): | ||||
|                     'max': 500000, | ||||
|                 }, | ||||
|             }, | ||||
|             'info': {}, | ||||
|             'info': { | ||||
|                 'leverage_buy': ['2', '3'], | ||||
|                 'leverage_sell': ['2', '3'], | ||||
|             }, | ||||
|         }, | ||||
|         'LTC/BTC': { | ||||
|             'id': 'ltcbtc', | ||||
| @@ -518,7 +545,10 @@ def get_markets(): | ||||
|                     'max': 500000, | ||||
|                 }, | ||||
|             }, | ||||
|             'info': {}, | ||||
|             'info': { | ||||
|                 'leverage_buy': [], | ||||
|                 'leverage_sell': [], | ||||
|             }, | ||||
|         }, | ||||
|         'XRP/BTC': { | ||||
|             'id': 'xrpbtc', | ||||
| @@ -596,7 +626,10 @@ def get_markets(): | ||||
|                     'max': None | ||||
|                 } | ||||
|             }, | ||||
|             'info': {}, | ||||
|             'info': { | ||||
|                 'leverage_buy': [], | ||||
|                 'leverage_sell': [], | ||||
|             }, | ||||
|         }, | ||||
|         'ETH/USDT': { | ||||
|             'id': 'USDT-ETH', | ||||
| @@ -712,6 +745,8 @@ def get_markets(): | ||||
|                     'max': None | ||||
|                 } | ||||
|             }, | ||||
|             'info': { | ||||
|             } | ||||
|         }, | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -1,21 +1,31 @@ | ||||
| from datetime import datetime, timezone | ||||
| from random import randint | ||||
| from unittest.mock import MagicMock | ||||
| from unittest.mock import MagicMock, PropertyMock | ||||
|  | ||||
| import ccxt | ||||
| import pytest | ||||
|  | ||||
| from freqtrade.enums import Collateral, TradingMode | ||||
| from freqtrade.exceptions import DependencyException, InvalidOrderException, OperationalException | ||||
| from tests.conftest import get_mock_coro, get_patched_exchange, log_has_re | ||||
| from tests.exchange.test_exchange import ccxt_exceptionhandlers | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize('limitratio,expected', [ | ||||
|     (None, 220 * 0.99), | ||||
|     (0.99, 220 * 0.99), | ||||
|     (0.98, 220 * 0.98), | ||||
| @pytest.mark.parametrize('limitratio,expected,side', [ | ||||
|     (None, 220 * 0.99, "sell"), | ||||
|     (0.99, 220 * 0.99, "sell"), | ||||
|     (0.98, 220 * 0.98, "sell"), | ||||
|     (None, 220 * 1.01, "buy"), | ||||
|     (0.99, 220 * 1.01, "buy"), | ||||
|     (0.98, 220 * 1.02, "buy"), | ||||
| ]) | ||||
| def test_stoploss_order_binance(default_conf, mocker, limitratio, expected): | ||||
| def test_stoploss_order_binance( | ||||
|     default_conf, | ||||
|     mocker, | ||||
|     limitratio, | ||||
|     expected, | ||||
|     side | ||||
| ): | ||||
|     api_mock = MagicMock() | ||||
|     order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6)) | ||||
|     order_type = 'stop_loss_limit' | ||||
| @@ -33,19 +43,32 @@ def test_stoploss_order_binance(default_conf, mocker, limitratio, expected): | ||||
|     exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance') | ||||
|  | ||||
|     with pytest.raises(OperationalException): | ||||
|         order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190, | ||||
|                                   order_types={'stoploss_on_exchange_limit_ratio': 1.05}) | ||||
|         order = exchange.stoploss( | ||||
|             pair='ETH/BTC', | ||||
|             amount=1, | ||||
|             stop_price=190, | ||||
|             side=side, | ||||
|             order_types={'stoploss_on_exchange_limit_ratio': 1.05}, | ||||
|             leverage=1.0 | ||||
|         ) | ||||
|  | ||||
|     api_mock.create_order.reset_mock() | ||||
|     order_types = {} if limitratio is None else {'stoploss_on_exchange_limit_ratio': limitratio} | ||||
|     order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types=order_types) | ||||
|     order = exchange.stoploss( | ||||
|         pair='ETH/BTC', | ||||
|         amount=1, | ||||
|         stop_price=220, | ||||
|         order_types=order_types, | ||||
|         side=side, | ||||
|         leverage=1.0 | ||||
|     ) | ||||
|  | ||||
|     assert 'id' in order | ||||
|     assert 'info' in order | ||||
|     assert order['id'] == order_id | ||||
|     assert api_mock.create_order.call_args_list[0][1]['symbol'] == 'ETH/BTC' | ||||
|     assert api_mock.create_order.call_args_list[0][1]['type'] == order_type | ||||
|     assert api_mock.create_order.call_args_list[0][1]['side'] == 'sell' | ||||
|     assert api_mock.create_order.call_args_list[0][1]['side'] == side | ||||
|     assert api_mock.create_order.call_args_list[0][1]['amount'] == 1 | ||||
|     # Price should be 1% below stopprice | ||||
|     assert api_mock.create_order.call_args_list[0][1]['price'] == expected | ||||
| @@ -55,17 +78,31 @@ def test_stoploss_order_binance(default_conf, mocker, limitratio, expected): | ||||
|     with pytest.raises(DependencyException): | ||||
|         api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance")) | ||||
|         exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance') | ||||
|         exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) | ||||
|         exchange.stoploss( | ||||
|             pair='ETH/BTC', | ||||
|             amount=1, | ||||
|             stop_price=220, | ||||
|             order_types={}, | ||||
|             side=side, | ||||
|             leverage=1.0) | ||||
|  | ||||
|     with pytest.raises(InvalidOrderException): | ||||
|         api_mock.create_order = MagicMock( | ||||
|             side_effect=ccxt.InvalidOrder("binance Order would trigger immediately.")) | ||||
|         exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance') | ||||
|         exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) | ||||
|         exchange.stoploss( | ||||
|             pair='ETH/BTC', | ||||
|             amount=1, | ||||
|             stop_price=220, | ||||
|             order_types={}, | ||||
|             side=side, | ||||
|             leverage=1.0 | ||||
|         ) | ||||
|  | ||||
|     ccxt_exceptionhandlers(mocker, default_conf, api_mock, "binance", | ||||
|                            "stoploss", "create_order", retries=1, | ||||
|                            pair='ETH/BTC', amount=1, stop_price=220, order_types={}) | ||||
|                            pair='ETH/BTC', amount=1, stop_price=220, order_types={}, | ||||
|                            side=side, leverage=1.0) | ||||
|  | ||||
|  | ||||
| def test_stoploss_order_dry_run_binance(default_conf, mocker): | ||||
| @@ -78,12 +115,25 @@ def test_stoploss_order_dry_run_binance(default_conf, mocker): | ||||
|     exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance') | ||||
|  | ||||
|     with pytest.raises(OperationalException): | ||||
|         order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190, | ||||
|                                   order_types={'stoploss_on_exchange_limit_ratio': 1.05}) | ||||
|         order = exchange.stoploss( | ||||
|             pair='ETH/BTC', | ||||
|             amount=1, | ||||
|             stop_price=190, | ||||
|             side="sell", | ||||
|             order_types={'stoploss_on_exchange_limit_ratio': 1.05}, | ||||
|             leverage=1.0 | ||||
|         ) | ||||
|  | ||||
|     api_mock.create_order.reset_mock() | ||||
|  | ||||
|     order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) | ||||
|     order = exchange.stoploss( | ||||
|         pair='ETH/BTC', | ||||
|         amount=1, | ||||
|         stop_price=220, | ||||
|         order_types={}, | ||||
|         side="sell", | ||||
|         leverage=1.0 | ||||
|     ) | ||||
|  | ||||
|     assert 'id' in order | ||||
|     assert 'info' in order | ||||
| @@ -94,18 +144,202 @@ def test_stoploss_order_dry_run_binance(default_conf, mocker): | ||||
|     assert order['amount'] == 1 | ||||
|  | ||||
|  | ||||
| def test_stoploss_adjust_binance(mocker, default_conf): | ||||
| @pytest.mark.parametrize('sl1,sl2,sl3,side', [ | ||||
|     (1501, 1499, 1501, "sell"), | ||||
|     (1499, 1501, 1499, "buy") | ||||
| ]) | ||||
| def test_stoploss_adjust_binance(mocker, default_conf, sl1, sl2, sl3, side): | ||||
|     exchange = get_patched_exchange(mocker, default_conf, id='binance') | ||||
|     order = { | ||||
|         'type': 'stop_loss_limit', | ||||
|         'price': 1500, | ||||
|         'info': {'stopPrice': 1500}, | ||||
|     } | ||||
|     assert exchange.stoploss_adjust(1501, order) | ||||
|     assert not exchange.stoploss_adjust(1499, order) | ||||
|     assert exchange.stoploss_adjust(sl1, order, side=side) | ||||
|     assert not exchange.stoploss_adjust(sl2, order, side=side) | ||||
|     # Test with invalid order case | ||||
|     order['type'] = 'stop_loss' | ||||
|     assert not exchange.stoploss_adjust(1501, order) | ||||
|     assert not exchange.stoploss_adjust(sl3, order, side=side) | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize('pair,nominal_value,max_lev', [ | ||||
|     ("BNB/BUSD", 0.0, 40.0), | ||||
|     ("BNB/USDT", 100.0, 153.84615384615384), | ||||
|     ("BTC/USDT", 170.30, 250.0), | ||||
|     ("BNB/BUSD", 999999.9, 10.0), | ||||
|     ("BNB/USDT", 5000000.0, 6.666666666666667), | ||||
|     ("BTC/USDT", 300000000.1, 2.0), | ||||
| ]) | ||||
| def test_get_max_leverage_binance(default_conf, mocker, pair, nominal_value, max_lev): | ||||
|     exchange = get_patched_exchange(mocker, default_conf, id="binance") | ||||
|     exchange._leverage_brackets = { | ||||
|         'BNB/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]], | ||||
|         'BNB/USDT': [[0.0, 0.0065], | ||||
|                      [10000.0, 0.01], | ||||
|                      [50000.0, 0.02], | ||||
|                      [250000.0, 0.05], | ||||
|                      [1000000.0, 0.1], | ||||
|                      [2000000.0, 0.125], | ||||
|                      [5000000.0, 0.15], | ||||
|                      [10000000.0, 0.25]], | ||||
|         '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]], | ||||
|     } | ||||
|     assert exchange.get_max_leverage(pair, nominal_value) == max_lev | ||||
|  | ||||
|  | ||||
| def test_fill_leverage_brackets_binance(default_conf, mocker): | ||||
|     api_mock = MagicMock() | ||||
|     api_mock.load_leverage_brackets = MagicMock(return_value={ | ||||
|         '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]], | ||||
|  | ||||
|     }) | ||||
|     default_conf['dry_run'] = False | ||||
|     default_conf['trading_mode'] = TradingMode.FUTURES | ||||
|     default_conf['collateral'] = Collateral.ISOLATED | ||||
|     exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance") | ||||
|     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]], | ||||
|     } | ||||
|  | ||||
|     api_mock = MagicMock() | ||||
|     api_mock.load_leverage_brackets = MagicMock() | ||||
|     type(api_mock).has = PropertyMock(return_value={'loadLeverageBrackets': True}) | ||||
|  | ||||
|     ccxt_exceptionhandlers( | ||||
|         mocker, | ||||
|         default_conf, | ||||
|         api_mock, | ||||
|         "binance", | ||||
|         "fill_leverage_brackets", | ||||
|         "load_leverage_brackets" | ||||
|     ) | ||||
|  | ||||
|  | ||||
| def test_fill_leverage_brackets_binance_dryrun(default_conf, mocker): | ||||
|     api_mock = MagicMock() | ||||
|     default_conf['trading_mode'] = TradingMode.FUTURES | ||||
|     default_conf['collateral'] = Collateral.ISOLATED | ||||
|     exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance") | ||||
|     exchange.fill_leverage_brackets() | ||||
|  | ||||
|     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] | ||||
|         ], | ||||
|         "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] | ||||
|         ], | ||||
|         "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] | ||||
|         ], | ||||
|         "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] | ||||
|         ] | ||||
|     } | ||||
|  | ||||
|     for key, value in leverage_brackets.items(): | ||||
|         assert exchange._leverage_brackets[key] == value | ||||
|  | ||||
|  | ||||
| def test__set_leverage_binance(mocker, default_conf): | ||||
|  | ||||
|     api_mock = MagicMock() | ||||
|     api_mock.set_leverage = MagicMock() | ||||
|     type(api_mock).has = PropertyMock(return_value={'setLeverage': True}) | ||||
|     default_conf['dry_run'] = False | ||||
|     exchange = get_patched_exchange(mocker, default_conf, id="binance") | ||||
|     exchange._set_leverage(3.0, trading_mode=TradingMode.MARGIN) | ||||
|  | ||||
|     ccxt_exceptionhandlers( | ||||
|         mocker, | ||||
|         default_conf, | ||||
|         api_mock, | ||||
|         "binance", | ||||
|         "_set_leverage", | ||||
|         "set_leverage", | ||||
|         pair="XRP/USDT", | ||||
|         leverage=5.0, | ||||
|         trading_mode=TradingMode.FUTURES | ||||
|     ) | ||||
|  | ||||
|  | ||||
| @pytest.mark.asyncio | ||||
| @@ -138,3 +372,15 @@ async def test__async_get_historic_ohlcv_binance(default_conf, mocker, caplog): | ||||
|     assert exchange._api_async.fetch_ohlcv.call_count == 2 | ||||
|     assert res == ohlcv | ||||
|     assert log_has_re(r"Candle-data for ETH/BTC available starting with .*", caplog) | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize("trading_mode,collateral,config", [ | ||||
|     ("", "", {}), | ||||
|     ("margin", "cross", {"options": {"defaultType": "margin"}}), | ||||
|     ("futures", "isolated", {"options": {"defaultType": "future"}}), | ||||
| ]) | ||||
| def test__ccxt_config(default_conf, mocker, trading_mode, collateral, config): | ||||
|     default_conf['trading_mode'] = trading_mode | ||||
|     default_conf['collateral'] = collateral | ||||
|     exchange = get_patched_exchange(mocker, default_conf, id="binance") | ||||
|     assert exchange._ccxt_config == config | ||||
|   | ||||
| @@ -11,6 +11,7 @@ import ccxt | ||||
| import pytest | ||||
| from pandas import DataFrame | ||||
|  | ||||
| from freqtrade.enums import Collateral, TradingMode | ||||
| from freqtrade.exceptions import (DDosProtection, DependencyException, InvalidOrderException, | ||||
|                                   OperationalException, PricingError, TemporaryError) | ||||
| from freqtrade.exchange import Binance, Bittrex, Exchange, Kraken | ||||
| @@ -131,6 +132,7 @@ def test_init_ccxt_kwargs(default_conf, mocker, caplog): | ||||
|  | ||||
|     assert log_has("Applying additional ccxt config: {'TestKWARG': 11, 'TestKWARG44': 11}", caplog) | ||||
|     assert ex._api.headers == {'hello': 'world'} | ||||
|     assert ex._ccxt_config == {} | ||||
|     Exchange._headers = {} | ||||
|  | ||||
|  | ||||
| @@ -395,7 +397,11 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: | ||||
|         PropertyMock(return_value=markets) | ||||
|     ) | ||||
|     result = exchange.get_min_pair_stake_amount('ETH/BTC', 1, stoploss) | ||||
|     assert isclose(result, 2 * (1+0.05) / (1-abs(stoploss))) | ||||
|     expected_result = 2 * (1+0.05) / (1-abs(stoploss)) | ||||
|     assert isclose(result, expected_result) | ||||
|     # With Leverage | ||||
|     result = exchange.get_min_pair_stake_amount('ETH/BTC', 1, stoploss, 3.0) | ||||
|     assert isclose(result, expected_result/3) | ||||
|  | ||||
|     # min amount is set | ||||
|     markets["ETH/BTC"]["limits"] = { | ||||
| @@ -407,7 +413,11 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: | ||||
|         PropertyMock(return_value=markets) | ||||
|     ) | ||||
|     result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss) | ||||
|     assert isclose(result, 2 * 2 * (1+0.05) / (1-abs(stoploss))) | ||||
|     expected_result = 2 * 2 * (1+0.05) / (1-abs(stoploss)) | ||||
|     assert isclose(result, expected_result) | ||||
|     # With Leverage | ||||
|     result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss, 5.0) | ||||
|     assert isclose(result, expected_result/5) | ||||
|  | ||||
|     # min amount and cost are set (cost is minimal) | ||||
|     markets["ETH/BTC"]["limits"] = { | ||||
| @@ -419,7 +429,11 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: | ||||
|         PropertyMock(return_value=markets) | ||||
|     ) | ||||
|     result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss) | ||||
|     assert isclose(result, max(2, 2 * 2) * (1+0.05) / (1-abs(stoploss))) | ||||
|     expected_result = max(2, 2 * 2) * (1+0.05) / (1-abs(stoploss)) | ||||
|     assert isclose(result, expected_result) | ||||
|     # With Leverage | ||||
|     result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss, 10) | ||||
|     assert isclose(result, expected_result/10) | ||||
|  | ||||
|     # min amount and cost are set (amount is minial) | ||||
|     markets["ETH/BTC"]["limits"] = { | ||||
| @@ -431,14 +445,26 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: | ||||
|         PropertyMock(return_value=markets) | ||||
|     ) | ||||
|     result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss) | ||||
|     assert isclose(result, max(8, 2 * 2) * (1+0.05) / (1-abs(stoploss))) | ||||
|     expected_result = max(8, 2 * 2) * (1+0.05) / (1-abs(stoploss)) | ||||
|     assert isclose(result, expected_result) | ||||
|     # With Leverage | ||||
|     result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss, 7.0) | ||||
|     assert isclose(result, expected_result/7.0) | ||||
|  | ||||
|     result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -0.4) | ||||
|     assert isclose(result, max(8, 2 * 2) * 1.5) | ||||
|     expected_result = max(8, 2 * 2) * 1.5 | ||||
|     assert isclose(result, expected_result) | ||||
|     # With Leverage | ||||
|     result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -0.4, 8.0) | ||||
|     assert isclose(result, expected_result/8.0) | ||||
|  | ||||
|     # Really big stoploss | ||||
|     result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -1) | ||||
|     assert isclose(result, max(8, 2 * 2) * 1.5) | ||||
|     expected_result = max(8, 2 * 2) * 1.5 | ||||
|     assert isclose(result, expected_result) | ||||
|     # With Leverage | ||||
|     result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -1, 12.0) | ||||
|     assert isclose(result, expected_result/12) | ||||
|  | ||||
|  | ||||
| def test_get_min_pair_stake_amount_real_data(mocker, default_conf) -> None: | ||||
| @@ -456,10 +482,10 @@ def test_get_min_pair_stake_amount_real_data(mocker, default_conf) -> None: | ||||
|         PropertyMock(return_value=markets) | ||||
|     ) | ||||
|     result = exchange.get_min_pair_stake_amount('ETH/BTC', 0.020405, stoploss) | ||||
|     assert round(result, 8) == round( | ||||
|         max(0.0001, 0.001 * 0.020405) * (1+0.05) / (1-abs(stoploss)), | ||||
|         8 | ||||
|     ) | ||||
|     expected_result = max(0.0001, 0.001 * 0.020405) * (1+0.05) / (1-abs(stoploss)) | ||||
|     assert round(result, 8) == round(expected_result, 8) | ||||
|     result = exchange.get_min_pair_stake_amount('ETH/BTC', 0.020405, stoploss, 3.0) | ||||
|     assert round(result, 8) == round(expected_result/3, 8) | ||||
|  | ||||
|  | ||||
| def test_set_sandbox(default_conf, mocker): | ||||
| @@ -970,7 +996,13 @@ def test_create_dry_run_order(default_conf, mocker, side, exchange_name): | ||||
|     exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) | ||||
|  | ||||
|     order = exchange.create_dry_run_order( | ||||
|         pair='ETH/BTC', ordertype='limit', side=side, amount=1, rate=200) | ||||
|         pair='ETH/BTC', | ||||
|         ordertype='limit', | ||||
|         side=side, | ||||
|         amount=1, | ||||
|         rate=200, | ||||
|         leverage=1.0 | ||||
|     ) | ||||
|     assert 'id' in order | ||||
|     assert f'dry_run_{side}_' in order["id"] | ||||
|     assert order["side"] == side | ||||
| @@ -993,7 +1025,13 @@ def test_create_dry_run_order_limit_fill(default_conf, mocker, side, startprice, | ||||
|                           ) | ||||
|  | ||||
|     order = exchange.create_dry_run_order( | ||||
|         pair='LTC/USDT', ordertype='limit', side=side, amount=1, rate=startprice) | ||||
|         pair='LTC/USDT', | ||||
|         ordertype='limit', | ||||
|         side=side, | ||||
|         amount=1, | ||||
|         rate=startprice, | ||||
|         leverage=1.0 | ||||
|     ) | ||||
|     assert order_book_l2_usd.call_count == 1 | ||||
|     assert 'id' in order | ||||
|     assert f'dry_run_{side}_' in order["id"] | ||||
| @@ -1039,7 +1077,13 @@ def test_create_dry_run_order_market_fill(default_conf, mocker, side, rate, amou | ||||
|                           ) | ||||
|  | ||||
|     order = exchange.create_dry_run_order( | ||||
|         pair='LTC/USDT', ordertype='market', side=side, amount=amount, rate=rate) | ||||
|         pair='LTC/USDT', | ||||
|         ordertype='market', | ||||
|         side=side, | ||||
|         amount=amount, | ||||
|         rate=rate, | ||||
|         leverage=1.0 | ||||
|     ) | ||||
|     assert 'id' in order | ||||
|     assert f'dry_run_{side}_' in order["id"] | ||||
|     assert order["side"] == side | ||||
| @@ -1049,10 +1093,7 @@ def test_create_dry_run_order_market_fill(default_conf, mocker, side, rate, amou | ||||
|     assert round(order["average"], 4) == round(endprice, 4) | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize("side", [ | ||||
|     ("buy"), | ||||
|     ("sell") | ||||
| ]) | ||||
| @pytest.mark.parametrize("side", ["buy", "sell"]) | ||||
| @pytest.mark.parametrize("ordertype,rate,marketprice", [ | ||||
|     ("market", None, None), | ||||
|     ("market", 200, True), | ||||
| @@ -1074,9 +1115,17 @@ def test_create_order(default_conf, mocker, side, ordertype, rate, marketprice, | ||||
|     mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y) | ||||
|     mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y) | ||||
|     exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) | ||||
|     exchange._set_leverage = MagicMock() | ||||
|     exchange.set_margin_mode = MagicMock() | ||||
|  | ||||
|     order = exchange.create_order( | ||||
|         pair='ETH/BTC', ordertype=ordertype, side=side, amount=1, rate=200) | ||||
|         pair='ETH/BTC', | ||||
|         ordertype=ordertype, | ||||
|         side=side, | ||||
|         amount=1, | ||||
|         rate=200, | ||||
|         leverage=1.0 | ||||
|     ) | ||||
|  | ||||
|     assert 'id' in order | ||||
|     assert 'info' in order | ||||
| @@ -1086,6 +1135,21 @@ def test_create_order(default_conf, mocker, side, ordertype, rate, marketprice, | ||||
|     assert api_mock.create_order.call_args[0][2] == side | ||||
|     assert api_mock.create_order.call_args[0][3] == 1 | ||||
|     assert api_mock.create_order.call_args[0][4] is rate | ||||
|     assert exchange._set_leverage.call_count == 0 | ||||
|     assert exchange.set_margin_mode.call_count == 0 | ||||
|  | ||||
|     exchange.trading_mode = TradingMode.FUTURES | ||||
|     order = exchange.create_order( | ||||
|         pair='ETH/BTC', | ||||
|         ordertype=ordertype, | ||||
|         side=side, | ||||
|         amount=1, | ||||
|         rate=200, | ||||
|         leverage=3.0 | ||||
|     ) | ||||
|  | ||||
|     assert exchange._set_leverage.call_count == 1 | ||||
|     assert exchange.set_margin_mode.call_count == 1 | ||||
|  | ||||
|  | ||||
| def test_buy_dry_run(default_conf, mocker): | ||||
| @@ -2624,10 +2688,17 @@ def test_get_fee(default_conf, mocker, exchange_name): | ||||
| def test_stoploss_order_unsupported_exchange(default_conf, mocker): | ||||
|     exchange = get_patched_exchange(mocker, default_conf, id='bittrex') | ||||
|     with pytest.raises(OperationalException, match=r"stoploss is not implemented .*"): | ||||
|         exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) | ||||
|         exchange.stoploss( | ||||
|             pair='ETH/BTC', | ||||
|             amount=1, | ||||
|             stop_price=220, | ||||
|             order_types={}, | ||||
|             side="sell", | ||||
|             leverage=1.0 | ||||
|         ) | ||||
|  | ||||
|     with pytest.raises(OperationalException, match=r"stoploss is not implemented .*"): | ||||
|         exchange.stoploss_adjust(1, {}) | ||||
|         exchange.stoploss_adjust(1, {}, side="sell") | ||||
|  | ||||
|  | ||||
| def test_merge_ft_has_dict(default_conf, mocker): | ||||
| @@ -2972,7 +3043,123 @@ def test_calculate_fee_rate(mocker, default_conf, order, expected) -> None: | ||||
|     (3, 5, 5), | ||||
|     (4, 5, 2), | ||||
|     (5, 5, 1), | ||||
|  | ||||
| ]) | ||||
| def test_calculate_backoff(retrycount, max_retries, expected): | ||||
|     assert calculate_backoff(retrycount, max_retries) == expected | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize('exchange', ['binance', 'kraken', 'ftx']) | ||||
| @pytest.mark.parametrize('stake_amount,leverage,min_stake_with_lev', [ | ||||
|     (9.0, 3.0, 3.0), | ||||
|     (20.0, 5.0, 4.0), | ||||
|     (100.0, 100.0, 1.0) | ||||
| ]) | ||||
| def test_get_stake_amount_considering_leverage( | ||||
|     exchange, | ||||
|     stake_amount, | ||||
|     leverage, | ||||
|     min_stake_with_lev, | ||||
|     mocker, | ||||
|     default_conf | ||||
| ): | ||||
|     exchange = get_patched_exchange(mocker, default_conf, id=exchange) | ||||
|     assert exchange._get_stake_amount_considering_leverage( | ||||
|         stake_amount, leverage) == min_stake_with_lev | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize("exchange_name,trading_mode", [ | ||||
|     ("binance", TradingMode.FUTURES), | ||||
|     ("ftx", TradingMode.MARGIN), | ||||
|     ("ftx", TradingMode.FUTURES) | ||||
| ]) | ||||
| def test__set_leverage(mocker, default_conf, exchange_name, trading_mode): | ||||
|  | ||||
|     api_mock = MagicMock() | ||||
|     api_mock.set_leverage = MagicMock() | ||||
|     type(api_mock).has = PropertyMock(return_value={'setLeverage': True}) | ||||
|     default_conf['dry_run'] = False | ||||
|  | ||||
|     ccxt_exceptionhandlers( | ||||
|         mocker, | ||||
|         default_conf, | ||||
|         api_mock, | ||||
|         exchange_name, | ||||
|         "_set_leverage", | ||||
|         "set_leverage", | ||||
|         pair="XRP/USDT", | ||||
|         leverage=5.0, | ||||
|         trading_mode=trading_mode | ||||
|     ) | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize("collateral", [ | ||||
|     (Collateral.CROSS), | ||||
|     (Collateral.ISOLATED) | ||||
| ]) | ||||
| def test_set_margin_mode(mocker, default_conf, collateral): | ||||
|  | ||||
|     api_mock = MagicMock() | ||||
|     api_mock.set_margin_mode = MagicMock() | ||||
|     type(api_mock).has = PropertyMock(return_value={'setMarginMode': True}) | ||||
|     default_conf['dry_run'] = False | ||||
|  | ||||
|     ccxt_exceptionhandlers( | ||||
|         mocker, | ||||
|         default_conf, | ||||
|         api_mock, | ||||
|         "binance", | ||||
|         "set_margin_mode", | ||||
|         "set_margin_mode", | ||||
|         pair="XRP/USDT", | ||||
|         collateral=collateral | ||||
|     ) | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize("exchange_name, trading_mode, collateral, exception_thrown", [ | ||||
|     ("binance", TradingMode.SPOT, None, False), | ||||
|     ("binance", TradingMode.MARGIN, Collateral.ISOLATED, True), | ||||
|     ("kraken", TradingMode.SPOT, None, False), | ||||
|     ("kraken", TradingMode.MARGIN, Collateral.ISOLATED, True), | ||||
|     ("kraken", TradingMode.FUTURES, Collateral.ISOLATED, True), | ||||
|     ("ftx", TradingMode.SPOT, None, False), | ||||
|     ("ftx", TradingMode.MARGIN, Collateral.ISOLATED, True), | ||||
|     ("ftx", TradingMode.FUTURES, Collateral.ISOLATED, True), | ||||
|     ("bittrex", TradingMode.SPOT, None, False), | ||||
|     ("bittrex", TradingMode.MARGIN, Collateral.CROSS, True), | ||||
|     ("bittrex", TradingMode.MARGIN, Collateral.ISOLATED, True), | ||||
|     ("bittrex", TradingMode.FUTURES, Collateral.CROSS, True), | ||||
|     ("bittrex", TradingMode.FUTURES, Collateral.ISOLATED, True), | ||||
|  | ||||
|     # TODO-lev: Remove once implemented | ||||
|     ("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), | ||||
|  | ||||
|     # TODO-lev: Uncomment once implemented | ||||
|     # ("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) | ||||
| ]) | ||||
| def test_validate_trading_mode_and_collateral( | ||||
|     default_conf, | ||||
|     mocker, | ||||
|     exchange_name, | ||||
|     trading_mode, | ||||
|     collateral, | ||||
|     exception_thrown | ||||
| ): | ||||
|     exchange = get_patched_exchange( | ||||
|         mocker, default_conf, id=exchange_name, mock_supported_modes=False) | ||||
|     if (exception_thrown): | ||||
|         with pytest.raises(OperationalException): | ||||
|             exchange.validate_trading_mode_and_collateral(trading_mode, collateral) | ||||
|     else: | ||||
|         exchange.validate_trading_mode_and_collateral(trading_mode, collateral) | ||||
|   | ||||
| @@ -14,7 +14,11 @@ from .test_exchange import ccxt_exceptionhandlers | ||||
| STOPLOSS_ORDERTYPE = 'stop' | ||||
|  | ||||
|  | ||||
| def test_stoploss_order_ftx(default_conf, mocker): | ||||
| @pytest.mark.parametrize('order_price,exchangelimitratio,side', [ | ||||
|     (217.8, 1.05, "sell"), | ||||
|     (222.2, 0.95, "buy"), | ||||
| ]) | ||||
| def test_stoploss_order_ftx(default_conf, mocker, order_price, exchangelimitratio, side): | ||||
|     api_mock = MagicMock() | ||||
|     order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6)) | ||||
|  | ||||
| @@ -32,12 +36,18 @@ def test_stoploss_order_ftx(default_conf, mocker): | ||||
|     exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx') | ||||
|  | ||||
|     # stoploss_on_exchange_limit_ratio is irrelevant for ftx market orders | ||||
|     order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190, | ||||
|                               order_types={'stoploss_on_exchange_limit_ratio': 1.05}) | ||||
|     order = exchange.stoploss( | ||||
|         pair='ETH/BTC', | ||||
|         amount=1, | ||||
|         stop_price=190, | ||||
|         side=side, | ||||
|         order_types={'stoploss_on_exchange_limit_ratio': exchangelimitratio}, | ||||
|         leverage=1.0 | ||||
|     ) | ||||
|  | ||||
|     assert api_mock.create_order.call_args_list[0][1]['symbol'] == 'ETH/BTC' | ||||
|     assert api_mock.create_order.call_args_list[0][1]['type'] == STOPLOSS_ORDERTYPE | ||||
|     assert api_mock.create_order.call_args_list[0][1]['side'] == 'sell' | ||||
|     assert api_mock.create_order.call_args_list[0][1]['side'] == side | ||||
|     assert api_mock.create_order.call_args_list[0][1]['amount'] == 1 | ||||
|     assert 'orderPrice' not in api_mock.create_order.call_args_list[0][1]['params'] | ||||
|     assert 'stopPrice' in api_mock.create_order.call_args_list[0][1]['params'] | ||||
| @@ -47,51 +57,79 @@ def test_stoploss_order_ftx(default_conf, mocker): | ||||
|  | ||||
|     api_mock.create_order.reset_mock() | ||||
|  | ||||
|     order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) | ||||
|     order = exchange.stoploss( | ||||
|         pair='ETH/BTC', | ||||
|         amount=1, | ||||
|         stop_price=220, | ||||
|         order_types={}, | ||||
|         side=side, | ||||
|         leverage=1.0 | ||||
|     ) | ||||
|  | ||||
|     assert 'id' in order | ||||
|     assert 'info' in order | ||||
|     assert order['id'] == order_id | ||||
|     assert api_mock.create_order.call_args_list[0][1]['symbol'] == 'ETH/BTC' | ||||
|     assert api_mock.create_order.call_args_list[0][1]['type'] == STOPLOSS_ORDERTYPE | ||||
|     assert api_mock.create_order.call_args_list[0][1]['side'] == 'sell' | ||||
|     assert api_mock.create_order.call_args_list[0][1]['side'] == side | ||||
|     assert api_mock.create_order.call_args_list[0][1]['amount'] == 1 | ||||
|     assert 'orderPrice' not in api_mock.create_order.call_args_list[0][1]['params'] | ||||
|     assert api_mock.create_order.call_args_list[0][1]['params']['stopPrice'] == 220 | ||||
|  | ||||
|     api_mock.create_order.reset_mock() | ||||
|     order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, | ||||
|                               order_types={'stoploss': 'limit'}) | ||||
|     order = exchange.stoploss( | ||||
|         pair='ETH/BTC', | ||||
|         amount=1, | ||||
|         stop_price=220, | ||||
|         order_types={'stoploss': 'limit'}, side=side, | ||||
|         leverage=1.0 | ||||
|     ) | ||||
|  | ||||
|     assert 'id' in order | ||||
|     assert 'info' in order | ||||
|     assert order['id'] == order_id | ||||
|     assert api_mock.create_order.call_args_list[0][1]['symbol'] == 'ETH/BTC' | ||||
|     assert api_mock.create_order.call_args_list[0][1]['type'] == STOPLOSS_ORDERTYPE | ||||
|     assert api_mock.create_order.call_args_list[0][1]['side'] == 'sell' | ||||
|     assert api_mock.create_order.call_args_list[0][1]['side'] == side | ||||
|     assert api_mock.create_order.call_args_list[0][1]['amount'] == 1 | ||||
|     assert 'orderPrice' in api_mock.create_order.call_args_list[0][1]['params'] | ||||
|     assert api_mock.create_order.call_args_list[0][1]['params']['orderPrice'] == 217.8 | ||||
|     assert api_mock.create_order.call_args_list[0][1]['params']['orderPrice'] == order_price | ||||
|     assert api_mock.create_order.call_args_list[0][1]['params']['stopPrice'] == 220 | ||||
|  | ||||
|     # test exception handling | ||||
|     with pytest.raises(DependencyException): | ||||
|         api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance")) | ||||
|         exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx') | ||||
|         exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) | ||||
|         exchange.stoploss( | ||||
|             pair='ETH/BTC', | ||||
|             amount=1, | ||||
|             stop_price=220, | ||||
|             order_types={}, | ||||
|             side=side, | ||||
|             leverage=1.0 | ||||
|         ) | ||||
|  | ||||
|     with pytest.raises(InvalidOrderException): | ||||
|         api_mock.create_order = MagicMock( | ||||
|             side_effect=ccxt.InvalidOrder("ftx Order would trigger immediately.")) | ||||
|         exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx') | ||||
|         exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) | ||||
|         exchange.stoploss( | ||||
|             pair='ETH/BTC', | ||||
|             amount=1, | ||||
|             stop_price=220, | ||||
|             order_types={}, | ||||
|             side=side, | ||||
|             leverage=1.0 | ||||
|         ) | ||||
|  | ||||
|     ccxt_exceptionhandlers(mocker, default_conf, api_mock, "ftx", | ||||
|                            "stoploss", "create_order", retries=1, | ||||
|                            pair='ETH/BTC', amount=1, stop_price=220, order_types={}) | ||||
|                            pair='ETH/BTC', amount=1, stop_price=220, order_types={}, | ||||
|                            side=side, leverage=1.0) | ||||
|  | ||||
|  | ||||
| def test_stoploss_order_dry_run_ftx(default_conf, mocker): | ||||
| @pytest.mark.parametrize('side', [("sell"), ("buy")]) | ||||
| def test_stoploss_order_dry_run_ftx(default_conf, mocker, side): | ||||
|     api_mock = MagicMock() | ||||
|     default_conf['dry_run'] = True | ||||
|     mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y) | ||||
| @@ -101,7 +139,14 @@ def test_stoploss_order_dry_run_ftx(default_conf, mocker): | ||||
|  | ||||
|     api_mock.create_order.reset_mock() | ||||
|  | ||||
|     order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) | ||||
|     order = exchange.stoploss( | ||||
|         pair='ETH/BTC', | ||||
|         amount=1, | ||||
|         stop_price=220, | ||||
|         order_types={}, | ||||
|         side=side, | ||||
|         leverage=1.0 | ||||
|     ) | ||||
|  | ||||
|     assert 'id' in order | ||||
|     assert 'info' in order | ||||
| @@ -112,20 +157,24 @@ def test_stoploss_order_dry_run_ftx(default_conf, mocker): | ||||
|     assert order['amount'] == 1 | ||||
|  | ||||
|  | ||||
| def test_stoploss_adjust_ftx(mocker, default_conf): | ||||
| @pytest.mark.parametrize('sl1,sl2,sl3,side', [ | ||||
|     (1501, 1499, 1501, "sell"), | ||||
|     (1499, 1501, 1499, "buy") | ||||
| ]) | ||||
| def test_stoploss_adjust_ftx(mocker, default_conf, sl1, sl2, sl3, side): | ||||
|     exchange = get_patched_exchange(mocker, default_conf, id='ftx') | ||||
|     order = { | ||||
|         'type': STOPLOSS_ORDERTYPE, | ||||
|         'price': 1500, | ||||
|     } | ||||
|     assert exchange.stoploss_adjust(1501, order) | ||||
|     assert not exchange.stoploss_adjust(1499, order) | ||||
|     assert exchange.stoploss_adjust(sl1, order, side=side) | ||||
|     assert not exchange.stoploss_adjust(sl2, order, side=side) | ||||
|     # Test with invalid order case ... | ||||
|     order['type'] = 'stop_loss_limit' | ||||
|     assert not exchange.stoploss_adjust(1501, order) | ||||
|     assert not exchange.stoploss_adjust(sl3, order, side=side) | ||||
|  | ||||
|  | ||||
| def test_fetch_stoploss_order(default_conf, mocker, limit_sell_order): | ||||
| def test_fetch_stoploss_order(default_conf, mocker, limit_sell_order, limit_buy_order): | ||||
|     default_conf['dry_run'] = True | ||||
|     order = MagicMock() | ||||
|     order.myid = 123 | ||||
| @@ -158,6 +207,16 @@ def test_fetch_stoploss_order(default_conf, mocker, limit_sell_order): | ||||
|     assert resp['type'] == 'stop' | ||||
|     assert resp['status_stop'] == 'triggered' | ||||
|  | ||||
|     api_mock.fetch_order = MagicMock(return_value=limit_buy_order) | ||||
|  | ||||
|     resp = exchange.fetch_stoploss_order('X', 'TKN/BTC') | ||||
|     assert resp | ||||
|     assert api_mock.fetch_order.call_count == 1 | ||||
|     assert resp['id_stop'] == 'mocked_limit_buy' | ||||
|     assert resp['id'] == 'X' | ||||
|     assert resp['type'] == 'stop' | ||||
|     assert resp['status_stop'] == 'triggered' | ||||
|  | ||||
|     with pytest.raises(InvalidOrderException): | ||||
|         api_mock.fetch_orders = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) | ||||
|         exchange = get_patched_exchange(mocker, default_conf, api_mock, id='ftx') | ||||
| @@ -191,3 +250,20 @@ def test_get_order_id(mocker, default_conf): | ||||
|         } | ||||
|     } | ||||
|     assert exchange.get_order_id_conditional(order) == '1111' | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize('pair,nominal_value,max_lev', [ | ||||
|     ("ADA/BTC", 0.0, 20.0), | ||||
|     ("BTC/EUR", 100.0, 20.0), | ||||
|     ("ZEC/USD", 173.31, 20.0), | ||||
| ]) | ||||
| def test_get_max_leverage_ftx(default_conf, mocker, pair, nominal_value, max_lev): | ||||
|     exchange = get_patched_exchange(mocker, default_conf, id="ftx") | ||||
|     assert exchange.get_max_leverage(pair, nominal_value) == max_lev | ||||
|  | ||||
|  | ||||
| def test_fill_leverage_brackets_ftx(default_conf, mocker): | ||||
|     # FTX only has one account wide leverage, so there's no leverage brackets | ||||
|     exchange = get_patched_exchange(mocker, default_conf, id="ftx") | ||||
|     exchange.fill_leverage_brackets() | ||||
|     assert exchange._leverage_brackets == {} | ||||
|   | ||||
| @@ -166,7 +166,11 @@ def test_get_balances_prod(default_conf, mocker): | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize('ordertype', ['market', 'limit']) | ||||
| def test_stoploss_order_kraken(default_conf, mocker, ordertype): | ||||
| @pytest.mark.parametrize('side,adjustedprice', [ | ||||
|     ("sell", 217.8), | ||||
|     ("buy", 222.2), | ||||
| ]) | ||||
| def test_stoploss_order_kraken(default_conf, mocker, ordertype, side, adjustedprice): | ||||
|     api_mock = MagicMock() | ||||
|     order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6)) | ||||
|  | ||||
| @@ -183,10 +187,17 @@ def test_stoploss_order_kraken(default_conf, mocker, ordertype): | ||||
|  | ||||
|     exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken') | ||||
|  | ||||
|     order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, | ||||
|                               order_types={'stoploss': ordertype, | ||||
|                                            'stoploss_on_exchange_limit_ratio': 0.99 | ||||
|                                            }) | ||||
|     order = exchange.stoploss( | ||||
|         pair='ETH/BTC', | ||||
|         amount=1, | ||||
|         stop_price=220, | ||||
|         side=side, | ||||
|         order_types={ | ||||
|             'stoploss': ordertype, | ||||
|             'stoploss_on_exchange_limit_ratio': 0.99 | ||||
|         }, | ||||
|         leverage=1.0 | ||||
|     ) | ||||
|  | ||||
|     assert 'id' in order | ||||
|     assert 'info' in order | ||||
| @@ -195,12 +206,14 @@ def test_stoploss_order_kraken(default_conf, mocker, ordertype): | ||||
|     if ordertype == 'limit': | ||||
|         assert api_mock.create_order.call_args_list[0][1]['type'] == STOPLOSS_LIMIT_ORDERTYPE | ||||
|         assert api_mock.create_order.call_args_list[0][1]['params'] == { | ||||
|             'trading_agreement': 'agree', 'price2': 217.8} | ||||
|             'trading_agreement': 'agree', | ||||
|             'price2': adjustedprice | ||||
|         } | ||||
|     else: | ||||
|         assert api_mock.create_order.call_args_list[0][1]['type'] == STOPLOSS_ORDERTYPE | ||||
|         assert api_mock.create_order.call_args_list[0][1]['params'] == { | ||||
|             'trading_agreement': 'agree'} | ||||
|     assert api_mock.create_order.call_args_list[0][1]['side'] == 'sell' | ||||
|     assert api_mock.create_order.call_args_list[0][1]['side'] == side | ||||
|     assert api_mock.create_order.call_args_list[0][1]['amount'] == 1 | ||||
|     assert api_mock.create_order.call_args_list[0][1]['price'] == 220 | ||||
|  | ||||
| @@ -208,20 +221,36 @@ def test_stoploss_order_kraken(default_conf, mocker, ordertype): | ||||
|     with pytest.raises(DependencyException): | ||||
|         api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance")) | ||||
|         exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken') | ||||
|         exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) | ||||
|         exchange.stoploss( | ||||
|             pair='ETH/BTC', | ||||
|             amount=1, | ||||
|             stop_price=220, | ||||
|             order_types={}, | ||||
|             side=side, | ||||
|             leverage=1.0 | ||||
|         ) | ||||
|  | ||||
|     with pytest.raises(InvalidOrderException): | ||||
|         api_mock.create_order = MagicMock( | ||||
|             side_effect=ccxt.InvalidOrder("kraken Order would trigger immediately.")) | ||||
|         exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken') | ||||
|         exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) | ||||
|         exchange.stoploss( | ||||
|             pair='ETH/BTC', | ||||
|             amount=1, | ||||
|             stop_price=220, | ||||
|             order_types={}, | ||||
|             side=side, | ||||
|             leverage=1.0 | ||||
|         ) | ||||
|  | ||||
|     ccxt_exceptionhandlers(mocker, default_conf, api_mock, "kraken", | ||||
|                            "stoploss", "create_order", retries=1, | ||||
|                            pair='ETH/BTC', amount=1, stop_price=220, order_types={}) | ||||
|                            pair='ETH/BTC', amount=1, stop_price=220, order_types={}, | ||||
|                            side=side, leverage=1.0) | ||||
|  | ||||
|  | ||||
| def test_stoploss_order_dry_run_kraken(default_conf, mocker): | ||||
| @pytest.mark.parametrize('side', ['buy', 'sell']) | ||||
| def test_stoploss_order_dry_run_kraken(default_conf, mocker, side): | ||||
|     api_mock = MagicMock() | ||||
|     default_conf['dry_run'] = True | ||||
|     mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y) | ||||
| @@ -231,7 +260,14 @@ def test_stoploss_order_dry_run_kraken(default_conf, mocker): | ||||
|  | ||||
|     api_mock.create_order.reset_mock() | ||||
|  | ||||
|     order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) | ||||
|     order = exchange.stoploss( | ||||
|         pair='ETH/BTC', | ||||
|         amount=1, | ||||
|         stop_price=220, | ||||
|         order_types={}, | ||||
|         side=side, | ||||
|         leverage=1.0 | ||||
|     ) | ||||
|  | ||||
|     assert 'id' in order | ||||
|     assert 'info' in order | ||||
| @@ -242,14 +278,54 @@ def test_stoploss_order_dry_run_kraken(default_conf, mocker): | ||||
|     assert order['amount'] == 1 | ||||
|  | ||||
|  | ||||
| def test_stoploss_adjust_kraken(mocker, default_conf): | ||||
| @pytest.mark.parametrize('sl1,sl2,sl3,side', [ | ||||
|     (1501, 1499, 1501, "sell"), | ||||
|     (1499, 1501, 1499, "buy") | ||||
| ]) | ||||
| def test_stoploss_adjust_kraken(mocker, default_conf, sl1, sl2, sl3, side): | ||||
|     exchange = get_patched_exchange(mocker, default_conf, id='kraken') | ||||
|     order = { | ||||
|         'type': STOPLOSS_ORDERTYPE, | ||||
|         'price': 1500, | ||||
|     } | ||||
|     assert exchange.stoploss_adjust(1501, order) | ||||
|     assert not exchange.stoploss_adjust(1499, order) | ||||
|     assert exchange.stoploss_adjust(sl1, order, side=side) | ||||
|     assert not exchange.stoploss_adjust(sl2, order, side=side) | ||||
|     # Test with invalid order case ... | ||||
|     order['type'] = 'stop_loss_limit' | ||||
|     assert not exchange.stoploss_adjust(1501, order) | ||||
|     assert not exchange.stoploss_adjust(sl3, order, side=side) | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize('pair,nominal_value,max_lev', [ | ||||
|     ("ADA/BTC", 0.0, 3.0), | ||||
|     ("BTC/EUR", 100.0, 5.0), | ||||
|     ("ZEC/USD", 173.31, 2.0), | ||||
| ]) | ||||
| def test_get_max_leverage_kraken(default_conf, mocker, pair, nominal_value, max_lev): | ||||
|     exchange = get_patched_exchange(mocker, default_conf, id="kraken") | ||||
|     exchange._leverage_brackets = { | ||||
|         'ADA/BTC': ['2', '3'], | ||||
|         'BTC/EUR': ['2', '3', '4', '5'], | ||||
|         'ZEC/USD': ['2'] | ||||
|     } | ||||
|     assert exchange.get_max_leverage(pair, nominal_value) == max_lev | ||||
|  | ||||
|  | ||||
| def test_fill_leverage_brackets_kraken(default_conf, mocker): | ||||
|     api_mock = MagicMock() | ||||
|     exchange = get_patched_exchange(mocker, default_conf, api_mock, id="kraken") | ||||
|     exchange.fill_leverage_brackets() | ||||
|  | ||||
|     assert exchange._leverage_brackets == { | ||||
|         'BLK/BTC': [1, 2, 3], | ||||
|         'TKN/BTC': [1, 2, 3, 4, 5], | ||||
|         'ETH/BTC': [1, 2], | ||||
|         'LTC/BTC': [1], | ||||
|         'XRP/BTC': [1], | ||||
|         'NEO/BTC': [1], | ||||
|         'BTT/BTC': [1], | ||||
|         'ETH/USDT': [1], | ||||
|         'LTC/USDT': [1], | ||||
|         'LTC/USD': [1], | ||||
|         'XLTCUSDT': [1], | ||||
|         'LTC/ETH': [1] | ||||
|     } | ||||
|   | ||||
| @@ -1252,6 +1252,7 @@ def test_create_stoploss_order_insufficient_funds(mocker, default_conf, caplog, | ||||
| @pytest.mark.usefixtures("init_persistence") | ||||
| def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, | ||||
|                                               limit_buy_order, limit_sell_order) -> None: | ||||
|     # TODO-lev: test for short | ||||
|     # When trailing stoploss is set | ||||
|     stoploss = MagicMock(return_value={'id': 13434334}) | ||||
|     patch_RPCManager(mocker) | ||||
| @@ -1343,10 +1344,14 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, | ||||
|     assert freqtrade.handle_stoploss_on_exchange(trade) is False | ||||
|  | ||||
|     cancel_order_mock.assert_called_once_with(100, 'ETH/BTC') | ||||
|     stoploss_order_mock.assert_called_once_with(amount=85.32423208, | ||||
|                                                 pair='ETH/BTC', | ||||
|                                                 order_types=freqtrade.strategy.order_types, | ||||
|                                                 stop_price=0.00002346 * 0.95) | ||||
|     stoploss_order_mock.assert_called_once_with( | ||||
|         amount=85.32423208, | ||||
|         pair='ETH/BTC', | ||||
|         order_types=freqtrade.strategy.order_types, | ||||
|         stop_price=0.00002346 * 0.95, | ||||
|         side="sell", | ||||
|         leverage=1.0 | ||||
|     ) | ||||
|  | ||||
|     # price fell below stoploss, so dry-run sells trade. | ||||
|     mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', MagicMock(return_value={ | ||||
| @@ -1359,6 +1364,7 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, | ||||
|  | ||||
| def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, caplog, | ||||
|                                                     limit_buy_order, limit_sell_order) -> None: | ||||
|     # TODO-lev: test for short | ||||
|     # When trailing stoploss is set | ||||
|     stoploss = MagicMock(return_value={'id': 13434334}) | ||||
|     patch_exchange(mocker) | ||||
| @@ -1417,7 +1423,7 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c | ||||
|                  side_effect=InvalidOrderException()) | ||||
|     mocker.patch('freqtrade.exchange.Binance.fetch_stoploss_order', | ||||
|                  return_value=stoploss_order_hanging) | ||||
|     freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging) | ||||
|     freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging, side="sell") | ||||
|     assert log_has_re(r"Could not cancel stoploss order abcd for pair ETH/BTC.*", caplog) | ||||
|  | ||||
|     # Still try to create order | ||||
| @@ -1427,7 +1433,7 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c | ||||
|     caplog.clear() | ||||
|     cancel_mock = mocker.patch("freqtrade.exchange.Binance.cancel_stoploss_order", MagicMock()) | ||||
|     mocker.patch("freqtrade.exchange.Binance.stoploss", side_effect=ExchangeError()) | ||||
|     freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging) | ||||
|     freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging, side="sell") | ||||
|     assert cancel_mock.call_count == 1 | ||||
|     assert log_has_re(r"Could not create trailing stoploss order for pair ETH/BTC\..*", caplog) | ||||
|  | ||||
| @@ -1436,6 +1442,7 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c | ||||
| def test_handle_stoploss_on_exchange_custom_stop(mocker, default_conf, fee, | ||||
|                                                  limit_buy_order, limit_sell_order) -> None: | ||||
|     # When trailing stoploss is set | ||||
|     # TODO-lev: test for short | ||||
|     stoploss = MagicMock(return_value={'id': 13434334}) | ||||
|     patch_RPCManager(mocker) | ||||
|     mocker.patch.multiple( | ||||
| @@ -1526,10 +1533,14 @@ def test_handle_stoploss_on_exchange_custom_stop(mocker, default_conf, fee, | ||||
|     assert freqtrade.handle_stoploss_on_exchange(trade) is False | ||||
|  | ||||
|     cancel_order_mock.assert_called_once_with(100, 'ETH/BTC') | ||||
|     stoploss_order_mock.assert_called_once_with(amount=85.32423208, | ||||
|                                                 pair='ETH/BTC', | ||||
|                                                 order_types=freqtrade.strategy.order_types, | ||||
|                                                 stop_price=0.00002346 * 0.96) | ||||
|     stoploss_order_mock.assert_called_once_with( | ||||
|         amount=85.32423208, | ||||
|         pair='ETH/BTC', | ||||
|         order_types=freqtrade.strategy.order_types, | ||||
|         stop_price=0.00002346 * 0.96, | ||||
|         side="sell", | ||||
|         leverage=1.0 | ||||
|     ) | ||||
|  | ||||
|     # price fell below stoploss, so dry-run sells trade. | ||||
|     mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', MagicMock(return_value={ | ||||
| @@ -1542,7 +1553,7 @@ def test_handle_stoploss_on_exchange_custom_stop(mocker, default_conf, fee, | ||||
|  | ||||
| def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog, | ||||
|                                               limit_buy_order, limit_sell_order) -> None: | ||||
|  | ||||
|     # TODO-lev: test for short | ||||
|     # When trailing stoploss is set | ||||
|     stoploss = MagicMock(return_value={'id': 13434334}) | ||||
|     patch_RPCManager(mocker) | ||||
| @@ -1647,10 +1658,14 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog, | ||||
|     # stoploss should be set to 1% as trailing is on | ||||
|     assert trade.stop_loss == 0.00002346 * 0.99 | ||||
|     cancel_order_mock.assert_called_once_with(100, 'NEO/BTC') | ||||
|     stoploss_order_mock.assert_called_once_with(amount=2132892.49146757, | ||||
|                                                 pair='NEO/BTC', | ||||
|                                                 order_types=freqtrade.strategy.order_types, | ||||
|                                                 stop_price=0.00002346 * 0.99) | ||||
|     stoploss_order_mock.assert_called_once_with( | ||||
|         amount=2132892.49146757, | ||||
|         pair='NEO/BTC', | ||||
|         order_types=freqtrade.strategy.order_types, | ||||
|         stop_price=0.00002346 * 0.99, | ||||
|         side="sell", | ||||
|         leverage=1.0 | ||||
|     ) | ||||
|  | ||||
|  | ||||
| def test_enter_positions(mocker, default_conf, caplog) -> None: | ||||
|   | ||||
		Reference in New Issue
	
	Block a user