Add stoploss support for huobi
This commit is contained in:
		| @@ -57,7 +57,7 @@ This configuration enables kraken, as well as rate-limiting to avoid bans from t | ||||
| Binance supports [time_in_force](configuration.md#understand-order_time_in_force). | ||||
|  | ||||
| !!! Tip "Stoploss on Exchange" | ||||
|     Binance supports `stoploss_on_exchange` and uses stop-loss-limit orders. It provides great advantages, so we recommend to benefit from it. | ||||
|     Binance 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.. | ||||
|  | ||||
| ### Binance Blacklist | ||||
|  | ||||
| @@ -71,6 +71,11 @@ Binance has been split into 2, and users must use the correct ccxt exchange ID f | ||||
| * [binance.com](https://www.binance.com/) - International users. Use exchange id: `binance`. | ||||
| * [binance.us](https://www.binance.us/) - US based users. Use exchange id: `binanceus`. | ||||
|  | ||||
| ## Huobi | ||||
|  | ||||
| !!! Tip "Stoploss on Exchange" | ||||
|     Huobi supports `stoploss_on_exchange` and uses `stop-limit` orders. It provides great advantages, so we recommend to benefit from it by enabling stoploss on exchange. | ||||
|  | ||||
| ## Kraken | ||||
|  | ||||
| !!! Tip "Stoploss on Exchange" | ||||
|   | ||||
| @@ -24,7 +24,7 @@ These modes can be configured with these values: | ||||
| ``` | ||||
|  | ||||
| !!! Note | ||||
|     Stoploss on exchange is only supported for Binance (stop-loss-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) and kucoin (stop-limit and stop-market) as of now.   | ||||
|     <ins>Do not set too low/tight stoploss value if using stop loss on exchange!</ins>   | ||||
|     If set to low/tight then you have greater risk of missing fill on the order and stoploss will not work. | ||||
|  | ||||
|   | ||||
| @@ -2,7 +2,12 @@ | ||||
| import logging | ||||
| from typing import Dict | ||||
|  | ||||
| import ccxt | ||||
|  | ||||
| from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException, | ||||
|                                   OperationalException, TemporaryError) | ||||
| from freqtrade.exchange import Exchange | ||||
| from freqtrade.exchange.common import retrier | ||||
|  | ||||
|  | ||||
| logger = logging.getLogger(__name__) | ||||
| @@ -15,5 +20,75 @@ class Huobi(Exchange): | ||||
|     """ | ||||
|  | ||||
|     _ft_has: Dict = { | ||||
|         "stoploss_on_exchange": True, | ||||
|         "ohlcv_candle_limit": 2000, | ||||
|     } | ||||
|  | ||||
|     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 order['type'] == 'stop' and stop_loss > float(order['stopPrice']) | ||||
|  | ||||
|     @retrier(retries=0) | ||||
|     def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict: | ||||
|         """ | ||||
|         creates a stoploss limit order. | ||||
|         this stoploss-limit is huobi-specific. | ||||
|         TODO: Compare this with other stoploss implementations - | ||||
|         """ | ||||
|         # 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 | ||||
|  | ||||
|         ordertype = "stop-limit" | ||||
|  | ||||
|         stop_price = self.price_to_precision(pair, stop_price) | ||||
|  | ||||
|         # Ensure rate is less than stop price | ||||
|         if stop_price <= rate: | ||||
|             raise OperationalException( | ||||
|                 'In stoploss limit order, stop price should be more than limit price') | ||||
|  | ||||
|         if self._config['dry_run']: | ||||
|             dry_order = self.create_dry_run_order( | ||||
|                 pair, ordertype, "sell", amount, stop_price) | ||||
|             return dry_order | ||||
|  | ||||
|         try: | ||||
|             params = self._params.copy() | ||||
|             params.update({ | ||||
|                 "stop-price": stop_price, | ||||
|                 "operator": "lte", | ||||
|                 }) | ||||
|  | ||||
|             amount = self.amount_to_precision(pair, amount) | ||||
|  | ||||
|             rate = self.price_to_precision(pair, rate) | ||||
|  | ||||
|             order = self._api.create_order(symbol=pair, type=ordertype, side='sell', | ||||
|                                            amount=amount, price=rate, params=params) | ||||
|             logger.info('stoploss limit order added for %s. ' | ||||
|                         'stop price: %s. limit: %s', pair, stop_price, rate) | ||||
|             self._log_exchange_response('create_stoploss_order', order) | ||||
|             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'Message: {e}') from e | ||||
|         except ccxt.InvalidOrder as e: | ||||
|             # Errors: | ||||
|             # `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'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 | ||||
|         except ccxt.BaseError as e: | ||||
|             raise OperationalException(e) from e | ||||
|   | ||||
							
								
								
									
										109
									
								
								tests/exchange/test_huobi.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										109
									
								
								tests/exchange/test_huobi.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,109 @@ | ||||
| from random import randint | ||||
| from unittest.mock import MagicMock | ||||
|  | ||||
| import ccxt | ||||
| import pytest | ||||
|  | ||||
| from freqtrade.exceptions import DependencyException, InvalidOrderException, OperationalException | ||||
| from tests.conftest import get_patched_exchange | ||||
| 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), | ||||
| ]) | ||||
| def test_stoploss_order_huobi(default_conf, mocker, limitratio, expected): | ||||
|     api_mock = MagicMock() | ||||
|     order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6)) | ||||
|     order_type = 'stop-limit' | ||||
|  | ||||
|     api_mock.create_order = MagicMock(return_value={ | ||||
|         'id': order_id, | ||||
|         'info': { | ||||
|             'foo': 'bar' | ||||
|         } | ||||
|     }) | ||||
|     default_conf['dry_run'] = False | ||||
|     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, 'huobi') | ||||
|  | ||||
|     with pytest.raises(OperationalException): | ||||
|         order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190, | ||||
|                                   order_types={'stoploss_on_exchange_limit_ratio': 1.05}) | ||||
|  | ||||
|     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) | ||||
|  | ||||
|     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]['amount'] == 1 | ||||
|     # Price should be 1% below stopprice | ||||
|     assert api_mock.create_order.call_args_list[0][1]['price'] == expected | ||||
|     assert api_mock.create_order.call_args_list[0][1]['params'] == {"stop-price": 220, | ||||
|                                                                     "operator": "lte", | ||||
|                                                                     } | ||||
|  | ||||
|     # 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, 'huobi') | ||||
|         exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) | ||||
|  | ||||
|     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={}) | ||||
|  | ||||
|     ccxt_exceptionhandlers(mocker, default_conf, api_mock, "huobi", | ||||
|                            "stoploss", "create_order", retries=1, | ||||
|                            pair='ETH/BTC', amount=1, stop_price=220, order_types={}) | ||||
|  | ||||
|  | ||||
| def test_stoploss_order_dry_run_huobi(default_conf, mocker): | ||||
|     api_mock = MagicMock() | ||||
|     order_type = 'stop-limit' | ||||
|     default_conf['dry_run'] = True | ||||
|     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, 'huobi') | ||||
|  | ||||
|     with pytest.raises(OperationalException): | ||||
|         order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190, | ||||
|                                   order_types={'stoploss_on_exchange_limit_ratio': 1.05}) | ||||
|  | ||||
|     api_mock.create_order.reset_mock() | ||||
|  | ||||
|     order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) | ||||
|  | ||||
|     assert 'id' in order | ||||
|     assert 'info' in order | ||||
|     assert 'type' in order | ||||
|  | ||||
|     assert order['type'] == order_type | ||||
|     assert order['price'] == 220 | ||||
|     assert order['amount'] == 1 | ||||
|  | ||||
|  | ||||
| def test_stoploss_adjust_huobi(mocker, default_conf): | ||||
|     exchange = get_patched_exchange(mocker, default_conf, id='huobi') | ||||
|     order = { | ||||
|         'type': 'stop', | ||||
|         'price': 1500, | ||||
|         'stopPrice': '1500', | ||||
|     } | ||||
|     assert exchange.stoploss_adjust(1501, order) | ||||
|     assert not exchange.stoploss_adjust(1499, order) | ||||
|     # Test with invalid order case | ||||
|     order['type'] = 'stop_loss' | ||||
|     assert not exchange.stoploss_adjust(1501, order) | ||||
		Reference in New Issue
	
	Block a user