Merge pull request #5850 from freqtrade/timeout_forcesell

multiple exit-timeouts can trigger emergencysell
This commit is contained in:
Matthias 2021-11-06 16:20:06 +01:00 committed by GitHub
commit fef7da03b2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 39 additions and 5 deletions

View File

@ -28,6 +28,7 @@
"unfilledtimeout": {
"buy": 10,
"sell": 30,
"exit_timeout_count": 0,
"unit": "minutes"
},
"bid_strategy": {

View File

@ -102,6 +102,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi
| `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).<br> **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).<br> **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). <br> *Defaults to `minutes`.* <br> **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).<br>*Defaults to `0`.* <br> **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).<br> *Defaults to `bid`.* <br> **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). <br> **Datatype:** Boolean

View File

@ -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'}
}
},

View File

@ -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)

View File

@ -491,6 +491,13 @@ 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.
"""
return len([o for o in self.orders if o.ft_order_side == 'sell'])
def _calc_open_trade_value(self) -> float:
"""
Calculate the open_rate including open_fee.
@ -775,7 +782,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.

View File

@ -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:

View File

@ -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)