diff --git a/docs/exchanges.md b/docs/exchanges.md index 8adf19081..c2368170d 100644 --- a/docs/exchanges.md +++ b/docs/exchanges.md @@ -210,6 +210,9 @@ OKX requires a passphrase for each api key, you will therefore need to add this ## Gate.io +!!! Tip "Stoploss on Exchange" + Gate.io supports `stoploss_on_exchange` and uses `stop-loss-limit` orders. It provides great advantages, so we recommend to benefit from it by enabling stoploss on exchange.. + Gate.io allows the use of `POINT` to pay for fees. As this is not a tradable currency (no regular market available), automatic fee calculations will fail (and default to a fee of 0). The configuration parameter `exchange.unknown_fee_rate` can be used to specify the exchange rate between Point and the stake currency. Obviously, changing the stake-currency will also require changes to this value. diff --git a/docs/stoploss.md b/docs/stoploss.md index d0e106d8f..62081b540 100644 --- a/docs/stoploss.md +++ b/docs/stoploss.md @@ -24,7 +24,7 @@ These modes can be configured with these values: ``` !!! Note - Stoploss on exchange is only supported for Binance (stop-loss-limit), Huobi (stop-limit), Kraken (stop-loss-market, stop-loss-limit), FTX (stop limit and stop-market) and kucoin (stop-limit and stop-market) as of now. + Stoploss on exchange is only supported for Binance (stop-loss-limit), Huobi (stop-limit), Kraken (stop-loss-market, stop-loss-limit), FTX (stop limit and stop-market) Gateio (stop-limit), and Kucoin (stop-limit and stop-market) as of now. Do not set too low/tight stoploss value if using stop loss on exchange! If set to low/tight then you have greater risk of missing fill on the order and stoploss will not work. diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index cfd12622b..a502ad034 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -376,7 +376,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( @@ -882,11 +882,11 @@ class Exchange: raise OperationalException(e) from e @retrier(retries=API_FETCH_ORDER_RETRY_COUNT) - def fetch_order(self, order_id: str, pair: str) -> Dict: + def fetch_order(self, order_id: str, pair: str, params={}) -> Dict: if self._config['dry_run']: return self.fetch_dry_run_order(order_id) try: - order = self._api.fetch_order(order_id, pair) + order = self._api.fetch_order(order_id, pair, params=params) self._log_exchange_response('fetch_order', order) return order except ccxt.OrderNotFound as e: @@ -929,7 +929,7 @@ class Exchange: and order.get('filled') == 0.0) @retrier - def cancel_order(self, order_id: str, pair: str) -> Dict: + def cancel_order(self, order_id: str, pair: str, params={}) -> Dict: if self._config['dry_run']: try: order = self.fetch_dry_run_order(order_id) @@ -940,7 +940,7 @@ class Exchange: return {} try: - order = self._api.cancel_order(order_id, pair) + order = self._api.cancel_order(order_id, pair, params=params) self._log_exchange_response('cancel_order', order) return order except ccxt.InvalidOrder as e: diff --git a/freqtrade/exchange/gateio.py b/freqtrade/exchange/gateio.py index 7e1f21921..bfe996e86 100644 --- a/freqtrade/exchange/gateio.py +++ b/freqtrade/exchange/gateio.py @@ -22,6 +22,8 @@ class Gateio(Exchange): _ft_has: Dict = { "ohlcv_candle_limit": 1000, "ohlcv_volume_currency": "quote", + "stoploss_order_types": {"limit": "limit"}, + "stoploss_on_exchange": True, } _headers = {'X-Gate-Channel-Id': 'freqtrade'} @@ -31,4 +33,25 @@ class Gateio(Exchange): if any(v == 'market' for k, v in order_types.items()): raise OperationalException( - f'Exchange {self.name} does not support market orders.') + f'Exchange {self.name} does not support market orders.') + + def fetch_stoploss_order(self, order_id: str, pair: str, params={}) -> Dict: + return self.fetch_order( + order_id=order_id, + pair=pair, + params={'stop': True} + ) + + def cancel_stoploss_order(self, order_id: str, pair: str, params={}) -> Dict: + return self.cancel_order( + order_id=order_id, + pair=pair, + params={'stop': True} + ) + + def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool: + """ + Verify stop_loss against stoploss-order value (limit or price) + Returns True if adjustment is necessary. + """ + return stop_loss > float(order['stopPrice']) diff --git a/tests/exchange/test_gateio.py b/tests/exchange/test_gateio.py index 6f7862909..ce356be8c 100644 --- a/tests/exchange/test_gateio.py +++ b/tests/exchange/test_gateio.py @@ -1,8 +1,11 @@ +from unittest.mock import MagicMock + import pytest from freqtrade.exceptions import OperationalException from freqtrade.exchange import Gateio from freqtrade.resolvers.exchange_resolver import ExchangeResolver +from tests.conftest import get_patched_exchange def test_validate_order_types_gateio(default_conf, mocker): @@ -26,3 +29,39 @@ def test_validate_order_types_gateio(default_conf, mocker): with pytest.raises(OperationalException, match=r'Exchange .* does not support market orders.'): ExchangeResolver.load_exchange('gateio', default_conf, True) + + +def test_fetch_stoploss_order_gateio(default_conf, mocker): + exchange = get_patched_exchange(mocker, default_conf, id='gateio') + + fetch_order_mock = MagicMock() + exchange.fetch_order = fetch_order_mock + + exchange.fetch_stoploss_order('1234', 'ETH/BTC') + assert fetch_order_mock.call_count == 1 + assert fetch_order_mock.call_args_list[0][1]['order_id'] == '1234' + assert fetch_order_mock.call_args_list[0][1]['pair'] == 'ETH/BTC' + assert fetch_order_mock.call_args_list[0][1]['params'] == {'stop': True} + + +def test_cancel_stoploss_order_gateio(default_conf, mocker): + exchange = get_patched_exchange(mocker, default_conf, id='gateio') + + cancel_order_mock = MagicMock() + exchange.cancel_order = cancel_order_mock + + exchange.cancel_stoploss_order('1234', 'ETH/BTC') + assert cancel_order_mock.call_count == 1 + assert cancel_order_mock.call_args_list[0][1]['order_id'] == '1234' + assert cancel_order_mock.call_args_list[0][1]['pair'] == 'ETH/BTC' + assert cancel_order_mock.call_args_list[0][1]['params'] == {'stop': True} + + +def test_stoploss_adjust_gateio(mocker, default_conf): + exchange = get_patched_exchange(mocker, default_conf, id='gateio') + order = { + 'price': 1500, + 'stopPrice': 1500, + } + assert exchange.stoploss_adjust(1501, order) + assert not exchange.stoploss_adjust(1499, order)