diff --git a/docs/leverage.md b/docs/leverage.md index c4b975a0b..9448c64c3 100644 --- a/docs/leverage.md +++ b/docs/leverage.md @@ -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 \ No newline at end of file diff --git a/freqtrade/exchange/bibox.py b/freqtrade/exchange/bibox.py index f0c2dd00b..074dd2b10 100644 --- a/freqtrade/exchange/bibox.py +++ b/freqtrade/exchange/bibox.py @@ -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}} diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 8dced3894..35f427c34 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -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 diff --git a/freqtrade/exchange/binance_leverage_brackets.json b/freqtrade/exchange/binance_leverage_brackets.json new file mode 100644 index 000000000..4450b015e --- /dev/null +++ b/freqtrade/exchange/binance_leverage_brackets.json @@ -0,0 +1,1214 @@ +{ + "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"] + ], + "ADA/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"] + ], + "AKRO/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"] + ], + "ALGO/USDT": [ + [0.0, "0.01"], + [50000.0, "0.025"], + [150000.0, "0.05"], + [250000.0, "0.1"], + [500000.0, "0.125"], + [1000000.0, "0.25"], + [2000000.0, "0.5"] + ], + "ALICE/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"] + ], + "ALPHA/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"] + ], + "ANKR/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"] + ], + "ATA/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"] + ], + "ATOM/USDT": [ + [0.0, "0.01"], + [50000.0, "0.025"], + [150000.0, "0.05"], + [250000.0, "0.1"], + [500000.0, "0.125"], + [1000000.0, "0.25"], + [2000000.0, "0.5"] + ], + "AUDIO/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"] + ], + "AVAX/USDT": [ + [0.0, "0.01"], + [50000.0, "0.025"], + [150000.0, "0.05"], + [250000.0, "0.1"], + [500000.0, "0.125"], + [750000.0, "0.25"], + [1000000.0, "0.5"] + ], + "AXS/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"], + [15000000.0, "0.5"] + ], + "BAKE/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"] + ], + "BAL/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"] + ], + "BAND/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"] + ], + "BAT/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"] + ], + "BCH/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"] + ], + "BEL/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"] + ], + "BLZ/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"] + ], + "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/BUSD": [ + [0.0, "0.004"], + [25000.0, "0.005"], + [100000.0, "0.01"], + [500000.0, "0.025"], + [1000000.0, "0.05"], + [2000000.0, "0.1"], + [5000000.0, "0.125"], + [10000000.0, "0.15"], + [20000000.0, "0.25"], + [30000000.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"] + ], + "BTCBUSD_210129": [ + [0.0, "0.004"], + [5000.0, "0.005"], + [25000.0, "0.01"], + [100000.0, "0.025"], + [500000.0, "0.05"], + [2000000.0, "0.1"], + [5000000.0, "0.125"], + [10000000.0, "0.15"], + [20000000.0, "0.25"] + ], + "BTCBUSD_210226": [ + [0.0, "0.004"], + [5000.0, "0.005"], + [25000.0, "0.01"], + [100000.0, "0.025"], + [500000.0, "0.05"], + [2000000.0, "0.1"], + [5000000.0, "0.125"], + [10000000.0, "0.15"], + [20000000.0, "0.25"] + ], + "BTCDOM/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"] + ], + "BTCSTUSDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "BTCUSDT_210326": [ + [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"] + ], + "BTCUSDT_210625": [ + [0.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "BTCUSDT_210924": [ + [0.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"], + [20000000.0, "0.5"] + ], + "BTS/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"] + ], + "BTT/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"] + ], + "BZRX/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"] + ], + "C98/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"] + ], + "CELR/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"] + ], + "CHR/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"] + ], + "CHZ/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"] + ], + "COMP/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"] + ], + "COTI/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"] + ], + "CRV/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"] + ], + "CTK/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"] + ], + "CVC/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"] + ], + "DASH/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"] + ], + "DEFI/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"] + ], + "DENT/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"] + ], + "DGB/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"] + ], + "DODO/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"] + ], + "DOGE/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"] + ], + "DOGE/USDT": [ + [0.0, "0.01"], + [50000.0, "0.025"], + [150000.0, "0.05"], + [250000.0, "0.1"], + [500000.0, "0.125"], + [750000.0, "0.25"], + [1000000.0, "0.5"] + ], + "DOT/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"] + ], + "DOTECOUSDT": [ + [0.0, "0.012"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "DYDX/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"] + ], + "EGLD/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"] + ], + "ENJ/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"] + ], + "EOS/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"] + ], + "ETC/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"] + ], + "ETH/BUSD": [ + [0.0, "0.004"], + [25000.0, "0.005"], + [100000.0, "0.01"], + [500000.0, "0.025"], + [1000000.0, "0.05"], + [2000000.0, "0.1"], + [5000000.0, "0.125"], + [10000000.0, "0.15"], + [20000000.0, "0.25"], + [30000000.0, "0.5"] + ], + "ETH/USDT": [ + [0.0, "0.005"], + [10000.0, "0.0065"], + [100000.0, "0.01"], + [500000.0, "0.02"], + [1000000.0, "0.05"], + [2000000.0, "0.1"], + [5000000.0, "0.125"], + [10000000.0, "0.15"], + [20000000.0, "0.25"] + ], + "ETHUSDT_210326": [ + [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"] + ], + "ETHUSDT_210625": [ + [0.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "ETHUSDT_210924": [ + [0.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"], + [20000000.0, "0.5"] + ], + "FIL/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"] + ], + "FLM/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"] + ], + "FTM/USDT": [ + [0.0, "0.01"], + [50000.0, "0.025"], + [150000.0, "0.05"], + [250000.0, "0.1"], + [500000.0, "0.125"], + [750000.0, "0.25"], + [1000000.0, "0.5"] + ], + "FTT/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"] + ], + "GRT/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"] + ], + "GTC/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"] + ], + "HBAR/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"] + ], + "HNT/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"] + ], + "HOT/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"] + ], + "ICP/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"] + ], + "ICX/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"] + ], + "IOST/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"] + ], + "IOTA/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"] + ], + "IOTX/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"] + ], + "KAVA/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"] + ], + "KEEP/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"] + ], + "KNC/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"] + ], + "KSM/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"] + ], + "LENDUSDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "LINA/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"] + ], + "LINK/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"] + ], + "LIT/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"] + ], + "LRC/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"] + ], + "LTC/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"] + ], + "LUNA/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"], + [15000000.0, "0.5"] + ], + "MANA/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"] + ], + "MASK/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"] + ], + "MATIC/USDT": [ + [0.0, "0.01"], + [50000.0, "0.025"], + [150000.0, "0.05"], + [250000.0, "0.1"], + [500000.0, "0.125"], + [750000.0, "0.25"], + [1000000.0, "0.5"] + ], + "MKR/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"] + ], + "MTL/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"] + ], + "NEAR/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"] + ], + "NEO/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"] + ], + "NKN/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"] + ], + "OCEAN/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"] + ], + "OGN/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"] + ], + "OMG/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"] + ], + "ONE/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"] + ], + "ONT/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"] + ], + "QTUM/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"] + ], + "RAY/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"] + ], + "REEF/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"] + ], + "REN/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"] + ], + "RLC/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"] + ], + "RSR/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"] + ], + "RUNE/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"] + ], + "RVN/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"] + ], + "SAND/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"] + ], + "SC/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"] + ], + "SFP/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"] + ], + "SKL/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"] + ], + "SNX/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"] + ], + "SOL/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"] + ], + "SOL/USDT": [ + [0.0, "0.01"], + [50000.0, "0.025"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.25"], + [10000000.0, "0.5"] + ], + "SRM/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"] + ], + "STMX/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"] + ], + "STORJ/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"] + ], + "SUSHI/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"] + ], + "SXP/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"] + ], + "THETA/USDT": [ + [0.0, "0.01"], + [50000.0, "0.025"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.1665"], + [10000000.0, "0.25"] + ], + "TLM/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"] + ], + "TOMO/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"] + ], + "TRB/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"] + ], + "TRX/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"] + ], + "UNFI/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"] + ], + "UNI/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"] + ], + "VET/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"] + ], + "WAVES/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"] + ], + "XEM/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"] + ], + "XLM/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"] + ], + "XMR/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"] + ], + "XRP/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"] + ], + "XRP/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"] + ], + "XTZ/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"] + ], + "YFI/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"] + ], + "YFII/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"] + ], + "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"] + ], + "ZEN/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"] + ], + "ZIL/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"] + ], + "ZRX/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"] + ] +} diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 2b9b08d70..4617fd4c2 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -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) diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index 6cd549d60..62adea04c 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -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 diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index 1b069aa6c..19d0a4967 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -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 diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index e14709324..2738ec634 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -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: diff --git a/tests/conftest.py b/tests/conftest.py index 609823409..d2f24fa69 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -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': { + } }, } diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index dd85c3abe..0c3e86fdd 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -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 diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 97bc33429..8b16a9f12 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -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) diff --git a/tests/exchange/test_ftx.py b/tests/exchange/test_ftx.py index 3794bb79c..ca6b24d64 100644 --- a/tests/exchange/test_ftx.py +++ b/tests/exchange/test_ftx.py @@ -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 == {} diff --git a/tests/exchange/test_kraken.py b/tests/exchange/test_kraken.py index eb79dfc10..a8cd8d8ef 100644 --- a/tests/exchange/test_kraken.py +++ b/tests/exchange/test_kraken.py @@ -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] + } diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index f278604be..28ca0ee49 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -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: