From 16dad8b6d4d4d40b8b1019f87612b8a27d37c243 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 30 Jan 2021 20:11:18 +0100 Subject: [PATCH] Allow custom_stoploss to cooperate with stoploss on exchange --- docs/configuration.md | 1 + freqtrade/freqtradebot.py | 3 +- freqtrade/resolvers/strategy_resolver.py | 1 + freqtrade/rpc/api_server/api_schemas.py | 1 + freqtrade/rpc/rpc.py | 1 + tests/test_freqtradebot.py | 103 +++++++++++++++++++++++ 6 files changed, 109 insertions(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index 660dd6171..509214b9c 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -136,6 +136,7 @@ Values set in the configuration file always overwrite values set in the strategy * `trailing_stop_positive` * `trailing_stop_positive_offset` * `trailing_only_offset_is_reached` +* `use_custom_stoploss` * `process_only_new_candles` * `order_types` * `order_time_in_force` diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index f45d4cacc..d7116834a 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -998,7 +998,8 @@ class FreqtradeBot(LoggingMixin): logger.warning('Stoploss order was cancelled, but unable to recreate one.') # Finally we check if stoploss on exchange should be moved up because of trailing. - if stoploss_order and self.config.get('trailing_stop', False): + if stoploss_order and (self.config.get('trailing_stop', False) + or self.config.get('use_custom_stoploss', False)): # 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 diff --git a/freqtrade/resolvers/strategy_resolver.py b/freqtrade/resolvers/strategy_resolver.py index 26a316873..b1b66e3ae 100644 --- a/freqtrade/resolvers/strategy_resolver.py +++ b/freqtrade/resolvers/strategy_resolver.py @@ -68,6 +68,7 @@ class StrategyResolver(IResolver): ("trailing_stop_positive", None, None), ("trailing_stop_positive_offset", 0.0, None), ("trailing_only_offset_is_reached", None, None), + ("use_custom_stoploss", None, None), ("process_only_new_candles", None, None), ("order_types", None, None), ("order_time_in_force", None, None), diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index c9e8aaceb..4faefb5fc 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -123,6 +123,7 @@ class ShowConfig(BaseModel): trailing_stop_positive: Optional[float] trailing_stop_positive_offset: Optional[float] trailing_only_offset_is_reached: Optional[bool] + use_custom_stoploss: Optional[bool] timeframe: str timeframe_ms: int timeframe_min: int diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 92cd6caf9..c5166c35e 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -129,6 +129,7 @@ class RPC: 'trailing_stop_positive': config.get('trailing_stop_positive'), 'trailing_stop_positive_offset': config.get('trailing_stop_positive_offset'), 'trailing_only_offset_is_reached': config.get('trailing_only_offset_is_reached'), + 'use_custom_stoploss': config.get('use_custom_stoploss'), 'bot_name': config.get('bot_name', 'freqtrade'), 'timeframe': config.get('timeframe'), 'timeframe_ms': timeframe_to_msecs(config['timeframe'] diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 06c08075b..2408afc87 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1570,6 +1570,109 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c assert log_has_re(r"Could not create trailing stoploss order for pair ETH/BTC\..*", caplog) +@pytest.mark.usefixtures("init_persistence") +def test_handle_stoploss_on_exchange_custom_stop(mocker, default_conf, fee, + limit_buy_order, limit_sell_order) -> None: + # When trailing stoploss is set + stoploss = MagicMock(return_value={'id': 13434334}) + patch_RPCManager(mocker) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + fetch_ticker=MagicMock(return_value={ + 'bid': 0.00001172, + 'ask': 0.00001173, + 'last': 0.00001172 + }), + buy=MagicMock(return_value={'id': limit_buy_order['id']}), + sell=MagicMock(return_value={'id': limit_sell_order['id']}), + get_fee=fee, + stoploss=stoploss, + stoploss_adjust=MagicMock(return_value=True), + ) + + # enabling TSL + default_conf['use_custom_stoploss'] = True + + # disabling ROI + default_conf['minimal_roi']['0'] = 999999999 + + freqtrade = get_patched_freqtradebot(mocker, default_conf) + + # enabling stoploss on exchange + freqtrade.strategy.order_types['stoploss_on_exchange'] = True + + # setting stoploss + freqtrade.strategy.custom_stoploss = lambda *args, **kwargs: -0.04 + + # setting stoploss_on_exchange_interval to 60 seconds + freqtrade.strategy.order_types['stoploss_on_exchange_interval'] = 60 + + patch_get_signal(freqtrade) + + freqtrade.enter_positions() + trade = Trade.query.first() + trade.is_open = True + trade.open_order_id = None + trade.stoploss_order_id = 100 + + stoploss_order_hanging = MagicMock(return_value={ + 'id': 100, + 'status': 'open', + 'type': 'stop_loss_limit', + 'price': 3, + 'average': 2, + 'info': { + 'stopPrice': '0.000011134' + } + }) + + mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', stoploss_order_hanging) + + assert freqtrade.handle_trade(trade) is False + assert freqtrade.handle_stoploss_on_exchange(trade) is False + + # price jumped 2x + mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', MagicMock(return_value={ + 'bid': 0.00002344, + 'ask': 0.00002346, + 'last': 0.00002344 + })) + + cancel_order_mock = MagicMock() + stoploss_order_mock = MagicMock(return_value={'id': 13434334}) + mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order', cancel_order_mock) + mocker.patch('freqtrade.exchange.Exchange.stoploss', stoploss_order_mock) + + # stoploss should not be updated as the interval is 60 seconds + assert freqtrade.handle_trade(trade) is False + assert freqtrade.handle_stoploss_on_exchange(trade) is False + cancel_order_mock.assert_not_called() + stoploss_order_mock.assert_not_called() + + assert freqtrade.handle_trade(trade) is False + assert trade.stop_loss == 0.00002346 * 0.96 + assert trade.stop_loss_pct == -0.04 + + # setting stoploss_on_exchange_interval to 0 seconds + freqtrade.strategy.order_types['stoploss_on_exchange_interval'] = 0 + + 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) + + # price fell below stoploss, so dry-run sells trade. + mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', MagicMock(return_value={ + 'bid': 0.00002144, + 'ask': 0.00002146, + 'last': 0.00002144 + })) + assert freqtrade.handle_trade(trade) is True + + def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog, limit_buy_order, limit_sell_order) -> None: