From 7a907a76362ab964cccd593d1970d3ab97116837 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 6 Nov 2021 11:48:49 +0100 Subject: [PATCH 1/3] Add Emergencyselling after X timeouts have been reached --- config_examples/config_full.example.json | 1 + docs/configuration.md | 1 + freqtrade/constants.py | 1 + freqtrade/freqtradebot.py | 9 ++++++++- freqtrade/persistence/models.py | 10 +++++++++- 5 files changed, 20 insertions(+), 2 deletions(-) diff --git a/config_examples/config_full.example.json b/config_examples/config_full.example.json index 83b8a27d0..228a08a02 100644 --- a/config_examples/config_full.example.json +++ b/config_examples/config_full.example.json @@ -28,6 +28,7 @@ "unfilledtimeout": { "buy": 10, "sell": 30, + "exit_timeout_count": 0, "unit": "minutes" }, "bid_strategy": { diff --git a/docs/configuration.md b/docs/configuration.md index 24198b44c..c566e33c2 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -101,6 +101,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `fee` | Fee used during backtesting / dry-runs. Should normally not be configured, which has freqtrade fall back to the exchange default fee. Set as ratio (e.g. 0.001 = 0.1%). Fee is applied twice for each trade, once when buying, once when selling.
**Datatype:** Float (as ratio) | `unfilledtimeout.buy` | **Required.** How long (in minutes or seconds) the bot will wait for an unfilled buy order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).
**Datatype:** Integer | `unfilledtimeout.sell` | **Required.** How long (in minutes or seconds) the bot will wait for an unfilled sell order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).
**Datatype:** Integer +| `unfilledtimeout.exit_timeout_count` | How many times can exit orders time out. 0 to disable and allow unlimited cancels. [Strategy Override](#parameters-in-the-strategy).
*Defaults to `0`.*
**Datatype:** Integer | `unfilledtimeout.unit` | Unit to use in unfilledtimeout setting. Note: If you set unfilledtimeout.unit to "seconds", "internals.process_throttle_secs" must be inferior or equal to timeout [Strategy Override](#parameters-in-the-strategy).
*Defaults to `minutes`.*
**Datatype:** String | `bid_strategy.price_side` | Select the side of the spread the bot should look at to get the buy rate. [More information below](#buy-price-side).
*Defaults to `bid`.*
**Datatype:** String (either `ask` or `bid`). | `bid_strategy.ask_last_balance` | **Required.** Interpolate the bidding price. More information [below](#buy-price-without-orderbook-enabled). diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 656893999..73830af09 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -156,6 +156,7 @@ CONF_SCHEMA = { 'properties': { 'buy': {'type': 'number', 'minimum': 1}, 'sell': {'type': 'number', 'minimum': 1}, + 'exit_timeout_count': {'type': 'number', 'minimum': 0, 'default': 0}, 'unit': {'type': 'string', 'enum': TIMEOUT_UNITS, 'default': 'minutes'} } }, diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index a1e8cad4a..a797b2e70 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -920,6 +920,13 @@ class FreqtradeBot(LoggingMixin): trade=trade, order=order))): self.handle_cancel_exit(trade, order, constants.CANCEL_REASON['TIMEOUT']) + canceled_count = trade.get_exit_order_count() + max_timeouts = self.config.get('unfilledtimeout', {}).get('exit_timeout_count', 0) + if max_timeouts > 0 and canceled_count >= max_timeouts: + logger.warning(f'Emergencyselling trade {trade}, as the sell order ' + f'timed out {max_timeouts} times.') + self.execute_trade_exit(trade, order.get('price'), sell_reason=SellCheckTuple( + sell_type=SellType.EMERGENCY_SELL)) def cancel_all_open_orders(self) -> None: """ @@ -1283,7 +1290,7 @@ class FreqtradeBot(LoggingMixin): if self.exchange.check_order_canceled_empty(order): # Trade has been cancelled on exchange - # Handling of this will happen in check_handle_timeout. + # Handling of this will happen in check_handle_timedout. return True # Try update amount (binance-fix) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index b3518c228..bef04fd76 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -491,6 +491,14 @@ class LocalTrade(): def update_order(self, order: Dict) -> None: Order.update_orders(self.orders, order) + def get_exit_order_count(self) -> int: + """ + Get amount of failed exiting orders + assumes full exits. + """ + orders = [o for o in self.orders if o.ft_order_side == 'sell'] + return len(orders) + def _calc_open_trade_value(self) -> float: """ Calculate the open_rate including open_fee. @@ -775,7 +783,7 @@ class Trade(_DECL_BASE, LocalTrade): return Trade.query @staticmethod - def get_open_order_trades(): + def get_open_order_trades() -> List['Trade']: """ Returns all open trades NOTE: Not supported in Backtesting. From f7dc47b1c8b17f61b05477c579a3a52fe355943c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 6 Nov 2021 13:10:41 +0100 Subject: [PATCH 2/3] Add test for exit_timeout_count --- freqtrade/persistence/models.py | 3 +-- tests/test_freqtradebot.py | 13 +++++++++++-- tests/test_persistence.py | 10 +++++++++- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index bef04fd76..61e59b7c3 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -496,8 +496,7 @@ class LocalTrade(): Get amount of failed exiting orders assumes full exits. """ - orders = [o for o in self.orders if o.ft_order_side == 'sell'] - return len(orders) + return len([o for o in self.orders if o.ft_order_side == 'sell']) def _calc_open_trade_value(self) -> float: """ diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 5acefd515..47b76af61 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -2134,11 +2134,12 @@ def test_check_handle_timedout_buy_exception(default_conf_usdt, ticker_usdt, def test_check_handle_timedout_sell_usercustom(default_conf_usdt, ticker_usdt, limit_sell_order_old, - mocker, open_trade) -> None: - default_conf_usdt["unfilledtimeout"] = {"buy": 1440, "sell": 1440} + mocker, open_trade, caplog) -> None: + default_conf_usdt["unfilledtimeout"] = {"buy": 1440, "sell": 1440, "exit_timeout_count": 1} rpc_mock = patch_RPCManager(mocker) cancel_order_mock = MagicMock() patch_exchange(mocker) + et_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.execute_trade_exit') mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker_usdt, @@ -2181,6 +2182,14 @@ def test_check_handle_timedout_sell_usercustom(default_conf_usdt, ticker_usdt, l assert open_trade.is_open is True assert freqtrade.strategy.check_sell_timeout.call_count == 1 + # 2nd canceled trade ... + caplog.clear() + open_trade.open_order_id = 'order_id_2' + mocker.patch('freqtrade.persistence.Trade.get_exit_order_count', return_value=1) + freqtrade.check_handle_timedout() + assert log_has_re('Emergencyselling trade.*', caplog) + assert et_mock.call_count == 1 + def test_check_handle_timedout_sell(default_conf_usdt, ticker_usdt, limit_sell_order_old, mocker, open_trade) -> None: diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 719dc8263..d1d3ce382 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -13,7 +13,7 @@ from sqlalchemy import create_engine, inspect, text from freqtrade import constants from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.persistence import LocalTrade, Order, Trade, clean_dry_run_db, init_db -from tests.conftest import create_mock_trades, log_has, log_has_re +from tests.conftest import create_mock_trades, create_mock_trades_usdt, log_has, log_has_re def test_init_create_session(default_conf): @@ -1190,6 +1190,14 @@ def test_get_best_pair(fee): assert res[1] == 0.01 +@pytest.mark.usefixtures("init_persistence") +def test_get_exit_order_count(fee): + + create_mock_trades_usdt(fee) + trade = Trade.get_trades([Trade.pair == 'ETC/USDT']).first() + assert trade.get_exit_order_count() == 1 + + @pytest.mark.usefixtures("init_persistence") def test_update_order_from_ccxt(caplog): # Most basic order return (only has orderid) From 4f5c5b6982bf21b6c72287c3270278c82e956903 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 6 Nov 2021 15:29:05 +0100 Subject: [PATCH 3/3] Clarify timeout documentation --- docs/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index c566e33c2..f2958f725 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -101,8 +101,8 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `fee` | Fee used during backtesting / dry-runs. Should normally not be configured, which has freqtrade fall back to the exchange default fee. Set as ratio (e.g. 0.001 = 0.1%). Fee is applied twice for each trade, once when buying, once when selling.
**Datatype:** Float (as ratio) | `unfilledtimeout.buy` | **Required.** How long (in minutes or seconds) the bot will wait for an unfilled buy order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).
**Datatype:** Integer | `unfilledtimeout.sell` | **Required.** How long (in minutes or seconds) the bot will wait for an unfilled sell order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).
**Datatype:** Integer -| `unfilledtimeout.exit_timeout_count` | How many times can exit orders time out. 0 to disable and allow unlimited cancels. [Strategy Override](#parameters-in-the-strategy).
*Defaults to `0`.*
**Datatype:** Integer | `unfilledtimeout.unit` | Unit to use in unfilledtimeout setting. Note: If you set unfilledtimeout.unit to "seconds", "internals.process_throttle_secs" must be inferior or equal to timeout [Strategy Override](#parameters-in-the-strategy).
*Defaults to `minutes`.*
**Datatype:** String +| `unfilledtimeout.exit_timeout_count` | How many times can exit orders time out. Once this number of timeouts is reached, an emergency sell is triggered. 0 to disable and allow unlimited order cancels. [Strategy Override](#parameters-in-the-strategy).
*Defaults to `0`.*
**Datatype:** Integer | `bid_strategy.price_side` | Select the side of the spread the bot should look at to get the buy rate. [More information below](#buy-price-side).
*Defaults to `bid`.*
**Datatype:** String (either `ask` or `bid`). | `bid_strategy.ask_last_balance` | **Required.** Interpolate the bidding price. More information [below](#buy-price-without-orderbook-enabled). | `bid_strategy.use_order_book` | Enable buying using the rates in [Order Book Bids](#buy-price-with-orderbook-enabled).
**Datatype:** Boolean