From 1d57ce19ebf3ba7be55aaebecb7db9db8d00c4cb Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 2 Feb 2022 19:45:49 +0100 Subject: [PATCH 1/8] Move stoploss -limit implemenentation to exchange class, as this seems to be used by multiple exchanges. --- freqtrade/exchange/binance.py | 64 +----------------------------- freqtrade/exchange/exchange.py | 71 +++++++++++++++++++++++++++++++--- 2 files changed, 67 insertions(+), 68 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 4ba30b626..a195788dd 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -3,12 +3,8 @@ import logging from typing import Dict, List, Tuple import arrow -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__) @@ -18,6 +14,7 @@ class Binance(Exchange): _ft_has: Dict = { "stoploss_on_exchange": True, + "stoploss_order_type": "stop_loss_limit", "order_time_in_force": ['gtc', 'fok', 'ioc'], "time_in_force_parameter": "timeInForce", "ohlcv_candle_limit": 1000, @@ -33,65 +30,6 @@ class Binance(Exchange): """ return order['type'] == 'stop_loss_limit' and stop_loss > float(order['info']['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 binance-specific. - It may work with a limited number of other exchanges, but this has not been tested yet. - """ - # 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_loss_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({'stopPrice': stop_price}) - - 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: - # `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'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 - async def _async_get_historic_ohlcv(self, pair: str, timeframe: str, since_ms: int, is_new_pair: bool = False, raise_: bool = False diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index a2217a02e..cd4c2ce83 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -791,18 +791,79 @@ class Exchange: """ raise OperationalException(f"stoploss is not implemented for {self.name}.") + @retrier(retries=0) def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict: """ creates a stoploss order. + creates a stoploss limit order. + Should an exchange support more ordertypes, the exchange should implement this method, + using `order_types.get('stoploss', 'market')` to get the correct ordertype (e.g. FTX). + The precise ordertype is determined by the order_types dict or exchange default. - Since ccxt does not unify stoploss-limit orders yet, this needs to be implemented in each - exchange's subclass. + The exception below should never raise, since we disallow starting the bot in validate_ordertypes() - Note: Changes to this interface need to be applied to all sub-classes too. - """ - raise OperationalException(f"stoploss is not implemented for {self.name}.") + This may work with a limited number of other exchanges, but correct working + needs to be tested individually. + WARNING: setting `stoploss_on_exchange` to True will NOT auto-enable stoploss on exchange. + `stoploss_adjust` must still be implemented for this to work. + """ + if not self._ft_has['stoploss_on_exchange']: + raise OperationalException(f"stoploss is not implemented for {self.name}.") + + # 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 = self._ft_has["stoploss_order_type"] + + 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() + # Verify if stopPrice works for your exchange! + params.update({'stopPrice': stop_price}) + + 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(f"stoploss limit order added for {pair}. " + f"stop price: {stop_price}. limit: {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 @retrier(retries=API_FETCH_ORDER_RETRY_COUNT) def fetch_order(self, order_id: str, pair: str) -> Dict: From ea197b79caaceb7f4f72ff9cfe2b2e069e4a16e2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 2 Feb 2022 20:40:40 +0100 Subject: [PATCH 2/8] Add some more logic to stoploss --- freqtrade/exchange/binance.py | 2 +- freqtrade/exchange/exchange.py | 28 +++++++++++++++++++--------- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index a195788dd..37ead6dd8 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -14,7 +14,7 @@ class Binance(Exchange): _ft_has: Dict = { "stoploss_on_exchange": True, - "stoploss_order_type": "stop_loss_limit", + "stoploss_order_types": {"limit": "stop_loss_limit"}, "order_time_in_force": ['gtc', 'fok', 'ioc'], "time_in_force_parameter": "timeInForce", "ohlcv_candle_limit": 1000, diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index cd4c2ce83..d8644dcb9 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -791,13 +791,18 @@ class Exchange: """ raise OperationalException(f"stoploss is not implemented for {self.name}.") + def _get_stop_params(self, ordertype: str, stop_price: float) -> Dict: + params = self._params.copy() + # Verify if stopPrice works for your exchange! + params.update({'stopPrice': stop_price}) + return params + @retrier(retries=0) def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict: """ creates a stoploss order. - creates a stoploss limit order. - Should an exchange support more ordertypes, the exchange should implement this method, - using `order_types.get('stoploss', 'market')` to get the correct ordertype (e.g. FTX). + requires `_ft_has['stoploss_order_types']` to be set as a dict mapping limit and market + to the corresponding exchange type. The precise ordertype is determined by the order_types dict or exchange default. @@ -812,12 +817,18 @@ class Exchange: if not self._ft_has['stoploss_on_exchange']: raise OperationalException(f"stoploss is not implemented for {self.name}.") - # Limit price threshold: As limit price should always be below stop-price + user_order_type = order_types.get('stoploss', 'market') + if user_order_type in self._ft_has["stoploss_order_types"].keys(): + ordertype = self._ft_has["stoploss_order_types"][user_order_type] + else: + # Otherwise pick only one available + ordertype = list(self._ft_has["stoploss_order_types"].values())[0] + + # if user_order_type == 'limit': + # 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 = self._ft_has["stoploss_order_type"] - stop_price = self.price_to_precision(pair, stop_price) # Ensure rate is less than stop price @@ -826,14 +837,13 @@ class Exchange: 'In stoploss limit order, stop price should be more than limit price') if self._config['dry_run']: + # TODO: will this work if ordertype is limit?? dry_order = self.create_dry_run_order( pair, ordertype, "sell", amount, stop_price) return dry_order try: - params = self._params.copy() - # Verify if stopPrice works for your exchange! - params.update({'stopPrice': stop_price}) + params = self._get_stop_params(ordertype=ordertype, stop_price=stop_price) amount = self.amount_to_precision(pair, amount) From 7ba92086c96a8b453b881825be1c2f789ff7b8f4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 3 Feb 2022 06:55:58 +0100 Subject: [PATCH 3/8] Make stoploss method more flexible --- freqtrade/exchange/exchange.py | 27 ++++++++++++++------------- freqtrade/freqtradebot.py | 2 +- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index d8644dcb9..60fd1ded4 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -823,27 +823,28 @@ class Exchange: else: # Otherwise pick only one available ordertype = list(self._ft_has["stoploss_order_types"].values())[0] + user_order_type = list(self._ft_has["stoploss_order_types"].keys())[0] - # if user_order_type == 'limit': + stop_price_norm = self.price_to_precision(pair, stop_price) + rate = None + if user_order_type == 'limit': # 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 + limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99) + rate = stop_price * limit_price_pct - 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') + # Ensure rate is less than stop price + if stop_price_norm <= rate: + raise OperationalException( + 'In stoploss limit order, stop price should be more than limit price') if self._config['dry_run']: # TODO: will this work if ordertype is limit?? dry_order = self.create_dry_run_order( - pair, ordertype, "sell", amount, stop_price) + pair, ordertype, "sell", amount, stop_price_norm) return dry_order try: - params = self._get_stop_params(ordertype=ordertype, stop_price=stop_price) + params = self._get_stop_params(ordertype=ordertype, stop_price=stop_price_norm) amount = self.amount_to_precision(pair, amount) @@ -851,7 +852,7 @@ class Exchange: order = self._api.create_order(symbol=pair, type=ordertype, side='sell', amount=amount, price=rate, params=params) - logger.info(f"stoploss limit order added for {pair}. " + logger.info(f"stoploss {user_order_type} order added for {pair}. " f"stop price: {stop_price}. limit: {rate}") self._log_exchange_response('create_stoploss_order', order) return order @@ -871,7 +872,7 @@ class Exchange: 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 stoploss order due to {e.__class__.__name__}. Message: {e}') from e except ccxt.BaseError as e: raise OperationalException(e) from e diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 20fd833eb..70cbc32b7 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -900,7 +900,7 @@ class FreqtradeBot(LoggingMixin): 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) -> None: """ Check to see if stoploss on exchange should be updated in case of trailing stoploss on exchange From 768b526c3867758c00234653d750eb9944f4eb8c Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 3 Feb 2022 07:57:58 +0100 Subject: [PATCH 4/8] Add kucoin stoploss on exchange --- freqtrade/exchange/kucoin.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/freqtrade/exchange/kucoin.py b/freqtrade/exchange/kucoin.py index 2884669a6..efb76f0e3 100644 --- a/freqtrade/exchange/kucoin.py +++ b/freqtrade/exchange/kucoin.py @@ -19,8 +19,27 @@ class Kucoin(Exchange): """ _ft_has: Dict = { + "stoploss_on_exchange": True, + "stoploss_order_types": {"limit": "limit", "market": "market"}, "l2_limit_range": [20, 100], "l2_limit_range_required": False, "order_time_in_force": ['gtc', 'fok', 'ioc'], "time_in_force_parameter": "timeInForce", } + + 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. + """ + # TODO: since kucoin uses Limit orders, changes to models will be required. + return order['info']['stop'] is not None and stop_loss > float(order['stopPrice']) + + def _get_stop_params(self, ordertype: str, stop_price: float) -> Dict: + + params = self._params.copy() + params.update({ + 'stopPrice': stop_price, + 'stop': 'loss' + }) + return params From 020729cf50b51eae5da8a77f0d97ea5e1a48b6d7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 24 Feb 2022 06:53:51 +0100 Subject: [PATCH 5/8] update docs about kucoin stoploss --- docs/exchanges.md | 4 ++++ docs/stoploss.md | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/exchanges.md b/docs/exchanges.md index e79abf220..a758245d2 100644 --- a/docs/exchanges.md +++ b/docs/exchanges.md @@ -177,6 +177,10 @@ Kucoin requires a passphrase for each api key, you will therefore need to add th Kucoin supports [time_in_force](configuration.md#understand-order_time_in_force). +!!! Tip "Stoploss on Exchange" + Kucoin supports `stoploss_on_exchange` and can use both stop-loss-market and stop-loss-limit orders. It provides great advantages, so we recommend to benefit from it. + You can use either `"limit"` or `"market"` in the `order_types.stoploss` configuration setting to decide which type of stoploss shall be used. + ### Kucoin Blacklists For Kucoin, please add `"KCS/"` to your blacklist to avoid issues. diff --git a/docs/stoploss.md b/docs/stoploss.md index 4d28846f1..0158e0365 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), Kraken (stop-loss-market, stop-loss-limit) and FTX (stop limit and stop-market) as of now. + 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. 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. From 07491990976ee885c7db63d469935cf83aa8cb38 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 24 Feb 2022 07:08:15 +0100 Subject: [PATCH 6/8] Add stoploss tests for kucoin --- freqtrade/exchange/exchange.py | 6 +- freqtrade/exchange/kucoin.py | 2 +- tests/exchange/test_kucoin.py | 120 +++++++++++++++++++++++++++++++++ 3 files changed, 124 insertions(+), 4 deletions(-) create mode 100644 tests/exchange/test_kucoin.py diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 60fd1ded4..b470f8ff2 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -836,6 +836,7 @@ class Exchange: if stop_price_norm <= rate: raise OperationalException( 'In stoploss limit order, stop price should be more than limit price') + rate = self.price_to_precision(pair, rate) if self._config['dry_run']: # TODO: will this work if ordertype is limit?? @@ -848,8 +849,6 @@ class Exchange: 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(f"stoploss {user_order_type} order added for {pair}. " @@ -872,7 +871,8 @@ class Exchange: raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( - f'Could not place stoploss order due to {e.__class__.__name__}. Message: {e}') from e + f"Could not place stoploss order due to {e.__class__.__name__}. " + f"Message: {e}") from e except ccxt.BaseError as e: raise OperationalException(e) from e diff --git a/freqtrade/exchange/kucoin.py b/freqtrade/exchange/kucoin.py index efb76f0e3..037ca5f9a 100644 --- a/freqtrade/exchange/kucoin.py +++ b/freqtrade/exchange/kucoin.py @@ -33,7 +33,7 @@ class Kucoin(Exchange): Returns True if adjustment is necessary. """ # TODO: since kucoin uses Limit orders, changes to models will be required. - return order['info']['stop'] is not None and stop_loss > float(order['stopPrice']) + return order['info'].get('stop') is not None and stop_loss > float(order['stopPrice']) def _get_stop_params(self, ordertype: str, stop_price: float) -> Dict: diff --git a/tests/exchange/test_kucoin.py b/tests/exchange/test_kucoin.py new file mode 100644 index 000000000..87f9ae8d9 --- /dev/null +++ b/tests/exchange/test_kucoin.py @@ -0,0 +1,120 @@ +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('order_type', ['market', 'limit']) +@pytest.mark.parametrize('limitratio,expected', [ + (None, 220 * 0.99), + (0.99, 220 * 0.99), + (0.98, 220 * 0.98), +]) +def test_stoploss_order_kucoin(default_conf, mocker, limitratio, expected, order_type): + api_mock = MagicMock() + order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6)) + + 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, 'kucoin') + if order_type == 'limit': + with pytest.raises(OperationalException): + order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190, + order_types={ + 'stoploss': order_type, + 'stoploss_on_exchange_limit_ratio': 1.05}) + + api_mock.create_order.reset_mock() + order_types = {'stoploss': order_type} + if limitratio is not None: + order_types.update({'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 + if order_type == 'limit': + assert api_mock.create_order.call_args_list[0][1]['price'] == expected + else: + assert api_mock.create_order.call_args_list[0][1]['price'] is None + + assert api_mock.create_order.call_args_list[0][1]['params'] == { + 'stopPrice': 220, + 'stop': 'loss' + } + + # 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, 'kucoin') + 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("kucoin Order would trigger immediately.")) + exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kucoin') + exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) + + ccxt_exceptionhandlers(mocker, default_conf, api_mock, "kucoin", + "stoploss", "create_order", retries=1, + pair='ETH/BTC', amount=1, stop_price=220, order_types={}) + + +def test_stoploss_order_dry_run_kucoin(default_conf, mocker): + api_mock = MagicMock() + order_type = 'market' + 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, 'kucoin') + + with pytest.raises(OperationalException): + order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190, + order_types={'stoploss': 'limit', + '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_kucoin(mocker, default_conf): + exchange = get_patched_exchange(mocker, default_conf, id='kucoin') + order = { + 'type': 'limit', + 'price': 1500, + 'stopPrice': 1500, + 'info': {'stopPrice': 1500, 'stop': "limit"}, + } + assert exchange.stoploss_adjust(1501, order) + assert not exchange.stoploss_adjust(1499, order) + # Test with invalid order case + order['info']['stop'] = None + assert not exchange.stoploss_adjust(1501, order) From 6caa5f7131eedc1f2cbc6b760e487c1064952af7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 25 Feb 2022 19:10:16 +0100 Subject: [PATCH 7/8] Update dry-run behaviour --- freqtrade/exchange/exchange.py | 19 ++++++++++++------- freqtrade/exchange/ftx.py | 2 +- freqtrade/exchange/kraken.py | 2 +- freqtrade/exchange/kucoin.py | 1 - freqtrade/freqtradebot.py | 4 ++-- 5 files changed, 16 insertions(+), 12 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index b470f8ff2..760a1dd32 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -600,7 +600,8 @@ class Exchange: # 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, params: Dict = {}, + stop_loss: bool = False) -> Dict[str, Any]: order_id = f'dry_run_{side}_{datetime.now().timestamp()}' _amount = self.amount_to_precision(pair, amount) dry_order: Dict[str, Any] = { @@ -616,14 +617,17 @@ class Exchange: 'remaining': _amount, 'datetime': arrow.utcnow().strftime('%Y-%m-%dT%H:%M:%S.%fZ'), 'timestamp': arrow.utcnow().int_timestamp * 1000, - 'status': "closed" if ordertype == "market" else "open", + 'status': "closed" if ordertype == "market" and not stop_loss else "open", 'fee': None, 'info': {} } - if dry_order["type"] in ["stop_loss_limit", "stop-loss-limit"]: + if stop_loss: dry_order["info"] = {"stopPrice": dry_order["price"]} + dry_order["stopPrice"] = dry_order["price"] + # Workaround to avoid filling stoploss orders immediately + dry_order["ft_order_type"] = "stoploss" - if dry_order["type"] == "market": + if dry_order["type"] == "market" and not dry_order.get("ft_order_type"): # Update market order pricing average = self.get_dry_market_fill_price(pair, side, amount, rate) dry_order.update({ @@ -714,7 +718,9 @@ class Exchange: """ Check dry-run limit order fill and update fee (if it filled). """ - if order['status'] != "closed" and order['type'] in ["limit"]: + if (order['status'] != "closed" + and order['type'] in ["limit"] + and not order.get('ft_order_type')): pair = order['symbol'] if self._is_dry_limit_order_filled(pair, order['side'], order['price']): order.update({ @@ -839,9 +845,8 @@ class Exchange: rate = self.price_to_precision(pair, rate) if self._config['dry_run']: - # TODO: will this work if ordertype is limit?? dry_order = self.create_dry_run_order( - pair, ordertype, "sell", amount, stop_price_norm) + pair, ordertype, "sell", amount, stop_price_norm, stop_loss=True) return dry_order try: diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index a8bf9abac..a346216b3 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -56,7 +56,7 @@ class Ftx(Exchange): if self._config['dry_run']: dry_order = self.create_dry_run_order( - pair, ordertype, "sell", amount, stop_price) + pair, ordertype, "sell", amount, stop_price, stop_loss=True) return dry_order try: diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index f4c8ca275..6a033f133 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -101,7 +101,7 @@ class Kraken(Exchange): if self._config['dry_run']: dry_order = self.create_dry_run_order( - pair, ordertype, "sell", amount, stop_price) + pair, ordertype, "sell", amount, stop_price, stop_loss=True) return dry_order try: diff --git a/freqtrade/exchange/kucoin.py b/freqtrade/exchange/kucoin.py index 037ca5f9a..e55f49cce 100644 --- a/freqtrade/exchange/kucoin.py +++ b/freqtrade/exchange/kucoin.py @@ -32,7 +32,6 @@ class Kucoin(Exchange): Verify stop_loss against stoploss-order value (limit or price) Returns True if adjustment is necessary. """ - # TODO: since kucoin uses Limit orders, changes to models will be required. return order['info'].get('stop') is not None and stop_loss > float(order['stopPrice']) def _get_stop_params(self, ordertype: str, stop_price: float) -> Dict: diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 70cbc32b7..e3214a61e 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1170,8 +1170,8 @@ class FreqtradeBot(LoggingMixin): # if stoploss is on exchange and we are on dry_run mode, # we consider the sell price stop price - if self.config['dry_run'] and sell_type == 'stoploss' \ - and self.strategy.order_types['stoploss_on_exchange']: + if (self.config['dry_run'] and sell_type == 'stoploss' + and self.strategy.order_types['stoploss_on_exchange']): limit = trade.stop_loss # set custom_exit_price if available From 3942b30ebf5b33ed5854b25a5b9ae91650c8ff1d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 26 Feb 2022 08:34:23 +0100 Subject: [PATCH 8/8] Add kraken TODO --- freqtrade/exchange/kraken.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index 6a033f133..8cec2500e 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -86,6 +86,8 @@ class Kraken(Exchange): """ Creates a stoploss market order. Stoploss market orders is the only stoploss type supported by kraken. + TODO: investigate if this can be combined with generic implementation + (careful, prices are reversed) """ params = self._params.copy()