Merge pull request #8086 from freqtrade/feat/cancel_order

Cancel open orders through UI/telegram
This commit is contained in:
Matthias 2023-02-06 19:43:21 +01:00 committed by GitHub
commit 82dad7ab17
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 160 additions and 18 deletions

View File

@ -163,7 +163,7 @@ python3 scripts/rest_client.py --config rest_config.json <command> [optional par
| `strategy <strategy>` | Get specific Strategy content. **Alpha** | `strategy <strategy>` | Get specific Strategy content. **Alpha**
| `available_pairs` | List available backtest data. **Alpha** | `available_pairs` | List available backtest data. **Alpha**
| `version` | Show version. | `version` | Show version.
| `sysinfo` | Show informations about the system load. | `sysinfo` | Show information about the system load.
| `health` | Show bot health (last bot loop). | `health` | Show bot health (last bot loop).
!!! Warning "Alpha status" !!! Warning "Alpha status"
@ -192,6 +192,11 @@ blacklist
:param add: List of coins to add (example: "BNB/BTC") :param add: List of coins to add (example: "BNB/BTC")
cancel_open_order
Cancel open order for trade.
:param trade_id: Cancels open orders for this trade.
count count
Return the amount of open trades. Return the amount of open trades.
@ -274,7 +279,6 @@ reload_config
Reload configuration. Reload configuration.
show_config show_config
Returns part of the configuration, relevant for trading operations. Returns part of the configuration, relevant for trading operations.
start start
@ -320,6 +324,7 @@ version
whitelist whitelist
Show the current whitelist. Show the current whitelist.
``` ```
### Message WebSocket ### Message WebSocket

View File

@ -162,26 +162,33 @@ official commands. You can ask at any moment for help with `/help`.
| Command | Description | | Command | Description |
|----------|-------------| |----------|-------------|
| **System commands**
| `/start` | Starts the trader | `/start` | Starts the trader
| `/stop` | Stops the trader | `/stop` | Stops the trader
| `/stopbuy | /stopentry` | Stops the trader from opening new trades. Gracefully closes open trades according to their rules. | `/stopbuy | /stopentry` | Stops the trader from opening new trades. Gracefully closes open trades according to their rules.
| `/reload_config` | Reloads the configuration file | `/reload_config` | Reloads the configuration file
| `/show_config` | Shows part of the current configuration with relevant settings to operation | `/show_config` | Shows part of the current configuration with relevant settings to operation
| `/logs [limit]` | Show last log messages. | `/logs [limit]` | Show last log messages.
| `/help` | Show help message
| `/version` | Show version
| **Status** |
| `/status` | Lists all open trades | `/status` | Lists all open trades
| `/status <trade_id>` | Lists one or more specific trade. Separate multiple <trade_id> with a blank space. | `/status <trade_id>` | Lists one or more specific trade. Separate multiple <trade_id> with a blank space.
| `/status table` | List all open trades in a table format. Pending buy orders are marked with an asterisk (*) Pending sell orders are marked with a double asterisk (**) | `/status table` | List all open trades in a table format. Pending buy orders are marked with an asterisk (*) Pending sell orders are marked with a double asterisk (**)
| `/trades [limit]` | List all recently closed trades in a table format. | `/trades [limit]` | List all recently closed trades in a table format.
| `/delete <trade_id>` | Delete a specific trade from the Database. Tries to close open orders. Requires manual handling of this trade on the exchange.
| `/count` | Displays number of trades used and available | `/count` | Displays number of trades used and available
| `/locks` | Show currently locked pairs. | `/locks` | Show currently locked pairs.
| `/unlock <pair or lock_id>` | Remove the lock for this pair (or for this lock id). | `/unlock <pair or lock_id>` | Remove the lock for this pair (or for this lock id).
| `/profit [<n>]` | Display a summary of your profit/loss from close trades and some stats about your performance, over the last n days (all trades by default) | **Modify Trade states** |
| `/forceexit <trade_id> | /fx <tradeid>` | Instantly exits the given trade (Ignoring `minimum_roi`). | `/forceexit <trade_id> | /fx <tradeid>` | Instantly exits the given trade (Ignoring `minimum_roi`).
| `/forceexit all | /fx all` | Instantly exits all open trades (Ignoring `minimum_roi`). | `/forceexit all | /fx all` | Instantly exits all open trades (Ignoring `minimum_roi`).
| `/fx` | alias for `/forceexit` | `/fx` | alias for `/forceexit`
| `/forcelong <pair> [rate]` | Instantly buys the given pair. Rate is optional and only applies to limit orders. (`force_entry_enable` must be set to True) | `/forcelong <pair> [rate]` | Instantly buys the given pair. Rate is optional and only applies to limit orders. (`force_entry_enable` must be set to True)
| `/forceshort <pair> [rate]` | Instantly shorts the given pair. Rate is optional and only applies to limit orders. This will only work on non-spot markets. (`force_entry_enable` must be set to True) | `/forceshort <pair> [rate]` | Instantly shorts the given pair. Rate is optional and only applies to limit orders. This will only work on non-spot markets. (`force_entry_enable` must be set to True)
| `/delete <trade_id>` | Delete a specific trade from the Database. Tries to close open orders. Requires manual handling of this trade on the exchange.
| `/cancel_open_order <trade_id> | /coo <trade_id>` | Cancel an open order for a trade.
| **Metrics** |
| `/profit [<n>]` | Display a summary of your profit/loss from close trades and some stats about your performance, over the last n days (all trades by default)
| `/performance` | Show performance of each finished trade grouped by pair | `/performance` | Show performance of each finished trade grouped by pair
| `/balance` | Show account balance per currency | `/balance` | Show account balance per currency
| `/daily <n>` | Shows profit or loss per day, over the last n days (n defaults to 7) | `/daily <n>` | Shows profit or loss per day, over the last n days (n defaults to 7)
@ -193,8 +200,7 @@ official commands. You can ask at any moment for help with `/help`.
| `/whitelist [sorted] [baseonly]` | Show the current whitelist. Optionally display in alphabetical order and/or with just the base currency of each pairing. | `/whitelist [sorted] [baseonly]` | Show the current whitelist. Optionally display in alphabetical order and/or with just the base currency of each pairing.
| `/blacklist [pair]` | Show the current blacklist, or adds a pair to the blacklist. | `/blacklist [pair]` | Show the current blacklist, or adds a pair to the blacklist.
| `/edge` | Show validated pairs by Edge if it is enabled. | `/edge` | Show validated pairs by Edge if it is enabled.
| `/help` | Show help message
| `/version` | Show version
## Telegram commands in action ## Telegram commands in action

View File

@ -363,7 +363,7 @@ class FreqtradeBot(LoggingMixin):
"Order is older than 5 days. Assuming order was fully cancelled.") "Order is older than 5 days. Assuming order was fully cancelled.")
fo = order.to_ccxt_object() fo = order.to_ccxt_object()
fo['status'] = 'canceled' fo['status'] = 'canceled'
self.handle_timedout_order(fo, order.trade) self.handle_cancel_order(fo, order.trade, constants.CANCEL_REASON['TIMEOUT'])
except ExchangeError as e: except ExchangeError as e:
@ -1263,11 +1263,11 @@ class FreqtradeBot(LoggingMixin):
if not_closed: if not_closed:
if fully_cancelled or (order_obj and self.strategy.ft_check_timed_out( if fully_cancelled or (order_obj and self.strategy.ft_check_timed_out(
trade, order_obj, datetime.now(timezone.utc))): trade, order_obj, datetime.now(timezone.utc))):
self.handle_timedout_order(order, trade) self.handle_cancel_order(order, trade, constants.CANCEL_REASON['TIMEOUT'])
else: else:
self.replace_order(order, order_obj, trade) self.replace_order(order, order_obj, trade)
def handle_timedout_order(self, order: Dict, trade: Trade) -> None: def handle_cancel_order(self, order: Dict, trade: Trade, reason: str) -> None:
""" """
Check if current analyzed order timed out and cancel if necessary. Check if current analyzed order timed out and cancel if necessary.
:param order: Order dict grabbed with exchange.fetch_order() :param order: Order dict grabbed with exchange.fetch_order()
@ -1275,10 +1275,10 @@ class FreqtradeBot(LoggingMixin):
:return: None :return: None
""" """
if order['side'] == trade.entry_side: if order['side'] == trade.entry_side:
self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['TIMEOUT']) self.handle_cancel_enter(trade, order, reason)
else: else:
canceled = self.handle_cancel_exit( canceled = self.handle_cancel_exit(
trade, order, constants.CANCEL_REASON['TIMEOUT']) trade, order, reason)
canceled_count = trade.get_exit_order_count() canceled_count = trade.get_exit_order_count()
max_timeouts = self.config.get('unfilledtimeout', {}).get('exit_timeout_count', 0) max_timeouts = self.config.get('unfilledtimeout', {}).get('exit_timeout_count', 0)
if canceled and max_timeouts > 0 and canceled_count >= max_timeouts: if canceled and max_timeouts > 0 and canceled_count >= max_timeouts:

View File

@ -41,7 +41,8 @@ logger = logging.getLogger(__name__)
# 2.21: Add new_candle messagetype # 2.21: Add new_candle messagetype
# 2.22: Add FreqAI to backtesting # 2.22: Add FreqAI to backtesting
# 2.23: Allow plot config request in webserver mode # 2.23: Allow plot config request in webserver mode
API_VERSION = 2.23 # 2.24: Add cancel_open_order endpoint
API_VERSION = 2.24
# Public API, requires no auth. # Public API, requires no auth.
router_public = APIRouter() router_public = APIRouter()
@ -123,6 +124,12 @@ def trades_delete(tradeid: int, rpc: RPC = Depends(get_rpc)):
return rpc._rpc_delete(tradeid) return rpc._rpc_delete(tradeid)
@router.delete('/trades/{tradeid}/open-order', response_model=OpenTradeSchema, tags=['trading'])
def cancel_open_order(tradeid: int, rpc: RPC = Depends(get_rpc)):
rpc._rpc_cancel_open_order(tradeid)
return rpc._rpc_trade_status([tradeid])[0]
# TODO: Missing response model # TODO: Missing response model
@router.get('/edge', tags=['info']) @router.get('/edge', tags=['info'])
def edge(rpc: RPC = Depends(get_rpc)): def edge(rpc: RPC = Depends(get_rpc)):

View File

@ -812,6 +812,29 @@ class RPC:
else: else:
raise RPCException(f'Failed to enter position for {pair}.') raise RPCException(f'Failed to enter position for {pair}.')
def _rpc_cancel_open_order(self, trade_id: int):
if self._freqtrade.state != State.RUNNING:
raise RPCException('trader is not running')
with self._freqtrade._exit_lock:
# Query for trade
trade = Trade.get_trades(
trade_filter=[Trade.id == trade_id, Trade.is_open.is_(True), ]
).first()
if not trade:
logger.warning('cancel_open_order: Invalid trade_id received.')
raise RPCException('Invalid trade_id.')
if not trade.open_order_id:
logger.warning('cancel_open_order: No open order for trade_id.')
raise RPCException('No open order for trade_id.')
try:
order = self._freqtrade.exchange.fetch_order(trade.open_order_id, trade.pair)
except ExchangeError as e:
logger.info(f"Cannot query order for {trade} due to {e}.", exc_info=True)
raise RPCException("Order not found.")
self._freqtrade.handle_cancel_order(order, trade, CANCEL_REASON['USER_CANCEL'])
Trade.commit()
def _rpc_delete(self, trade_id: int) -> Dict[str, Union[str, int]]: def _rpc_delete(self, trade_id: int) -> Dict[str, Union[str, int]]:
""" """
Handler for delete <id>. Handler for delete <id>.

View File

@ -174,6 +174,7 @@ class Telegram(RPCHandler):
self._force_enter, order_side=SignalDirection.SHORT)), self._force_enter, order_side=SignalDirection.SHORT)),
CommandHandler('trades', self._trades), CommandHandler('trades', self._trades),
CommandHandler('delete', self._delete_trade), CommandHandler('delete', self._delete_trade),
CommandHandler(['coo', 'cancel_open_order'], self._cancel_open_order),
CommandHandler('performance', self._performance), CommandHandler('performance', self._performance),
CommandHandler(['buys', 'entries'], self._enter_tag_performance), CommandHandler(['buys', 'entries'], self._enter_tag_performance),
CommandHandler(['sells', 'exits'], self._exit_reason_performance), CommandHandler(['sells', 'exits'], self._exit_reason_performance),
@ -1144,10 +1145,25 @@ class Telegram(RPCHandler):
raise RPCException("Trade-id not set.") raise RPCException("Trade-id not set.")
trade_id = int(context.args[0]) trade_id = int(context.args[0])
msg = self._rpc._rpc_delete(trade_id) msg = self._rpc._rpc_delete(trade_id)
self._send_msg(( self._send_msg(
f"`{msg['result_msg']}`\n" f"`{msg['result_msg']}`\n"
'Please make sure to take care of this asset on the exchange manually.' 'Please make sure to take care of this asset on the exchange manually.'
)) )
@authorized_only
def _cancel_open_order(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /cancel_open_order <id>.
Cancel open order for tradeid
:param bot: telegram bot
:param update: message update
:return: None
"""
if not context.args or len(context.args) == 0:
raise RPCException("Trade-id not set.")
trade_id = int(context.args[0])
self._rpc._rpc_cancel_open_order(trade_id)
self._send_msg('Open order canceled.')
@authorized_only @authorized_only
def _performance(self, update: Update, context: CallbackContext) -> None: def _performance(self, update: Update, context: CallbackContext) -> None:
@ -1456,6 +1472,10 @@ class Telegram(RPCHandler):
"*/fx <trade_id>|all:* `Alias to /forceexit`\n" "*/fx <trade_id>|all:* `Alias to /forceexit`\n"
f"{force_enter_text if self._config.get('force_entry_enable', False) else ''}" f"{force_enter_text if self._config.get('force_entry_enable', False) else ''}"
"*/delete <trade_id>:* `Instantly delete the given trade in the database`\n" "*/delete <trade_id>:* `Instantly delete the given trade in the database`\n"
"*/cancel_open_order <trade_id>:* `Cancels open orders for trade. "
"Only valid when the trade has open orders.`\n"
"*/coo <trade_id>|all:* `Alias to /cancel_open_order`\n"
"*/whitelist [sorted] [baseonly]:* `Show current whitelist. Optionally in " "*/whitelist [sorted] [baseonly]:* `Show current whitelist. Optionally in "
"order and/or only displaying the base currency of each pairing.`\n" "order and/or only displaying the base currency of each pairing.`\n"
"*/blacklist [pair]:* `Show current blacklist, or adds one or more pairs " "*/blacklist [pair]:* `Show current blacklist, or adds one or more pairs "

View File

@ -177,8 +177,7 @@ class FtRestClient():
return self._get("version") return self._get("version")
def show_config(self): def show_config(self):
""" """ Returns part of the configuration, relevant for trading operations.
Returns part of the configuration, relevant for trading operations.
:return: json object containing the version :return: json object containing the version
""" """
return self._get("show_config") return self._get("show_config")
@ -232,6 +231,14 @@ class FtRestClient():
""" """
return self._delete(f"trades/{trade_id}") return self._delete(f"trades/{trade_id}")
def cancel_open_order(self, trade_id):
"""Cancel open order for trade.
:param trade_id: Cancels open orders for this trade.
:return: json object
"""
return self._delete(f"trades/{trade_id}/open-order")
def whitelist(self): def whitelist(self):
"""Show the current whitelist. """Show the current whitelist.

View File

@ -706,6 +706,46 @@ def test_api_delete_trade(botclient, mocker, fee, markets, is_short):
assert_response(rc, 502) assert_response(rc, 502)
@pytest.mark.parametrize('is_short', [True, False])
def test_api_delete_open_order(botclient, mocker, fee, markets, ticker, is_short):
ftbot, client = botclient
patch_get_signal(ftbot, enter_long=not is_short, enter_short=is_short)
stoploss_mock = MagicMock()
cancel_mock = MagicMock()
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
markets=PropertyMock(return_value=markets),
fetch_ticker=ticker,
cancel_order=cancel_mock,
cancel_stoploss_order=stoploss_mock,
)
rc = client_delete(client, f"{BASE_URI}/trades/10/open-order")
assert_response(rc, 502)
assert 'Invalid trade_id.' in rc.json()['error']
create_mock_trades(fee, is_short=is_short)
Trade.commit()
rc = client_delete(client, f"{BASE_URI}/trades/5/open-order")
assert_response(rc, 502)
assert 'No open order for trade_id' in rc.json()['error']
trade = Trade.get_trades([Trade.id == 6]).first()
mocker.patch('freqtrade.exchange.Exchange.fetch_order',
side_effect=ExchangeError)
rc = client_delete(client, f"{BASE_URI}/trades/6/open-order")
assert_response(rc, 502)
assert 'Order not found.' in rc.json()['error']
trade = Trade.get_trades([Trade.id == 6]).first()
mocker.patch('freqtrade.exchange.Exchange.fetch_order',
return_value=trade.orders[-1].to_ccxt_object())
rc = client_delete(client, f"{BASE_URI}/trades/6/open-order")
assert_response(rc)
assert cancel_mock.call_count == 1
def test_api_logs(botclient): def test_api_logs(botclient):
ftbot, client = botclient ftbot, client = botclient
rc = client_get(client, f"{BASE_URI}/logs") rc = client_get(client, f"{BASE_URI}/logs")

View File

@ -99,7 +99,7 @@ def test_telegram_init(default_conf, mocker, caplog) -> None:
message_str = ("rpc.telegram is listening for following commands: [['status'], ['profit'], " message_str = ("rpc.telegram is listening for following commands: [['status'], ['profit'], "
"['balance'], ['start'], ['stop'], " "['balance'], ['start'], ['stop'], "
"['forcesell', 'forceexit', 'fx'], ['forcebuy', 'forcelong'], ['forceshort'], " "['forcesell', 'forceexit', 'fx'], ['forcebuy', 'forcelong'], ['forceshort'], "
"['trades'], ['delete'], ['performance'], " "['trades'], ['delete'], ['coo', 'cancel_open_order'], ['performance'], "
"['buys', 'entries'], ['sells', 'exits'], ['mix_tags'], " "['buys', 'entries'], ['sells', 'exits'], ['mix_tags'], "
"['stats'], ['daily'], ['weekly'], ['monthly'], " "['stats'], ['daily'], ['weekly'], ['monthly'], "
"['count'], ['locks'], ['unlock', 'delete_locks'], " "['count'], ['locks'], ['unlock', 'delete_locks'], "
@ -1678,6 +1678,40 @@ def test_telegram_delete_trade(mocker, update, default_conf, fee, is_short):
assert "Please make sure to take care of this asset" in msg_mock.call_args_list[0][0][0] assert "Please make sure to take care of this asset" in msg_mock.call_args_list[0][0][0]
@pytest.mark.parametrize('is_short', [True, False])
def test_telegram_delete_open_order(mocker, update, default_conf, fee, is_short, ticker):
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
fetch_ticker=ticker,
)
telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
context = MagicMock()
context.args = []
telegram._cancel_open_order(update=update, context=context)
assert "Trade-id not set." in msg_mock.call_args_list[0][0][0]
msg_mock.reset_mock()
create_mock_trades(fee, is_short=is_short)
context = MagicMock()
context.args = [5]
telegram._cancel_open_order(update=update, context=context)
assert "No open order for trade_id" in msg_mock.call_args_list[0][0][0]
msg_mock.reset_mock()
trade = Trade.get_trades([Trade.id == 6]).first()
mocker.patch('freqtrade.exchange.Exchange.fetch_order',
return_value=trade.orders[-1].to_ccxt_object())
context = MagicMock()
context.args = [6]
telegram._cancel_open_order(update=update, context=context)
assert msg_mock.call_count == 1
assert "Open order canceled." in msg_mock.call_args_list[0][0][0]
def test_help_handle(default_conf, update, mocker) -> None: def test_help_handle(default_conf, update, mocker) -> None:
telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)

View File

@ -5028,7 +5028,7 @@ def test_startup_update_open_orders(mocker, default_conf_usdt, fee, caplog, is_s
assert log_has_re(r"Error updating Order .*", caplog) assert log_has_re(r"Error updating Order .*", caplog)
mocker.patch('freqtrade.exchange.Exchange.fetch_order', side_effect=InvalidOrderException) mocker.patch('freqtrade.exchange.Exchange.fetch_order', side_effect=InvalidOrderException)
hto_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_timedout_order') hto_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_cancel_order')
# Orders which are no longer found after X days should be assumed as canceled. # Orders which are no longer found after X days should be assumed as canceled.
freqtrade.startup_update_open_orders() freqtrade.startup_update_open_orders()
assert log_has_re(r"Order is older than \d days.*", caplog) assert log_has_re(r"Order is older than \d days.*", caplog)