Merge pull request #6615 from cyberjunky/cyber-forcesell-tg
Add selection buttons for trades to forcesell cmd in telegram
This commit is contained in:
commit
1951ee8e29
@ -275,9 +275,12 @@ The relative profit of `1.2%` is the average profit per trade.
|
||||
The relative profit of `15.2 Σ%` is be based on the starting capital - so in this case, the starting capital was `0.00485701 * 1.152 = 0.00738 BTC`.
|
||||
Starting capital is either taken from the `available_capital` setting, or calculated by using current wallet size - profits.
|
||||
|
||||
### /forcesell <trade_id>
|
||||
### /forceexit <trade_id>
|
||||
|
||||
> **BINANCE:** Selling BTC/LTC with limit `0.01650000 (profit: ~-4.07%, -0.00008168)`
|
||||
> **BINANCE:** Exiting BTC/LTC with limit `0.01650000 (profit: ~-4.07%, -0.00008168)`
|
||||
|
||||
!!! Tip
|
||||
You can get a list of all open trades by calling `/forceexit` without parameter, which will show a list of buttons to simply exit a trade.
|
||||
|
||||
### /forcelong <pair> [rate] | /forceshort <pair> [rate]
|
||||
|
||||
|
@ -103,7 +103,6 @@ class Telegram(RPCHandler):
|
||||
['/count', '/start', '/stop', '/help']
|
||||
]
|
||||
# do not allow commands with mandatory arguments and critical cmds
|
||||
# like /forcesell and /forcebuy
|
||||
# TODO: DRY! - its not good to list all valid cmds here. But otherwise
|
||||
# this needs refactoring of the whole telegram module (same
|
||||
# problem in _help()).
|
||||
@ -116,6 +115,7 @@ class Telegram(RPCHandler):
|
||||
r'/logs$', r'/whitelist$', r'/blacklist$', r'/bl_delete$',
|
||||
r'/weekly$', r'/weekly \d+$', r'/monthly$', r'/monthly \d+$',
|
||||
r'/forcebuy$', r'/forcelong$', r'/forceshort$',
|
||||
r'/forcesell$', r'/forceexit$',
|
||||
r'/edge$', r'/health$', r'/help$', r'/version$']
|
||||
# Create keys for generation
|
||||
valid_keys_print = [k.replace('$', '') for k in valid_keys]
|
||||
@ -197,7 +197,8 @@ class Telegram(RPCHandler):
|
||||
pattern='update_exit_reason_performance'),
|
||||
CallbackQueryHandler(self._mix_tag_performance, pattern='update_mix_tag_performance'),
|
||||
CallbackQueryHandler(self._count, pattern='update_count'),
|
||||
CallbackQueryHandler(self._force_enter_inline),
|
||||
CallbackQueryHandler(self._force_exit_inline, pattern=r"force_exit__\S+"),
|
||||
CallbackQueryHandler(self._force_enter_inline, pattern=r"\S+\/\S+"),
|
||||
]
|
||||
for handle in handles:
|
||||
self._updater.dispatcher.add_handler(handle)
|
||||
@ -928,23 +929,58 @@ class Telegram(RPCHandler):
|
||||
@authorized_only
|
||||
def _force_exit(self, update: Update, context: CallbackContext) -> None:
|
||||
"""
|
||||
Handler for /forcesell <id>.
|
||||
Handler for /forceexit <id>.
|
||||
Sells the given trade at current price
|
||||
:param bot: telegram bot
|
||||
:param update: message update
|
||||
:return: None
|
||||
"""
|
||||
|
||||
trade_id = context.args[0] if context.args and len(context.args) > 0 else None
|
||||
if not trade_id:
|
||||
self._send_msg("You must specify a trade-id or 'all'.")
|
||||
return
|
||||
try:
|
||||
msg = self._rpc._rpc_force_exit(trade_id)
|
||||
self._send_msg('Force_exit Result: `{result}`'.format(**msg))
|
||||
if context.args:
|
||||
trade_id = context.args[0]
|
||||
self._force_exit_action(trade_id)
|
||||
else:
|
||||
fiat_currency = self._config.get('fiat_display_currency', '')
|
||||
try:
|
||||
statlist, head, fiat_profit_sum = self._rpc._rpc_status_table(
|
||||
self._config['stake_currency'], fiat_currency)
|
||||
except RPCException:
|
||||
self._send_msg(msg='No open trade found.')
|
||||
return
|
||||
trades = []
|
||||
for trade in statlist:
|
||||
trades.append((trade[0], f"{trade[0]} {trade[1]} {trade[2]} {trade[3]}"))
|
||||
|
||||
except RPCException as e:
|
||||
self._send_msg(str(e))
|
||||
trade_buttons = [
|
||||
InlineKeyboardButton(text=trade[1], callback_data=f"force_exit__{trade[0]}")
|
||||
for trade in trades]
|
||||
buttons_aligned = self._layout_inline_keyboard_onecol(trade_buttons)
|
||||
|
||||
buttons_aligned.append([InlineKeyboardButton(
|
||||
text='Cancel', callback_data='force_exit__cancel')])
|
||||
self._send_msg(msg="Which trade?", keyboard=buttons_aligned)
|
||||
|
||||
def _force_exit_action(self, trade_id):
|
||||
if trade_id != 'cancel':
|
||||
try:
|
||||
self._rpc._rpc_force_exit(trade_id)
|
||||
except RPCException as e:
|
||||
self._send_msg(str(e))
|
||||
|
||||
def _force_exit_inline(self, update: Update, _: CallbackContext) -> None:
|
||||
if update.callback_query:
|
||||
query = update.callback_query
|
||||
if query.data and '__' in query.data:
|
||||
# Input data is "force_exit__<tradid|cancel>"
|
||||
trade_id = query.data.split("__")[1].split(' ')[0]
|
||||
if trade_id == 'cancel':
|
||||
query.answer()
|
||||
query.edit_message_text(text="Force exit canceled.")
|
||||
return
|
||||
trade: Trade = Trade.get_trades(trade_filter=Trade.id == trade_id).first()
|
||||
query.answer()
|
||||
query.edit_message_text(text=f"Manually exiting Trade #{trade_id}, {trade.pair}")
|
||||
self._force_exit_action(trade_id)
|
||||
|
||||
def _force_enter_action(self, pair, price: Optional[float], order_side: SignalDirection):
|
||||
if pair != 'cancel':
|
||||
@ -964,8 +1000,13 @@ class Telegram(RPCHandler):
|
||||
self._force_enter_action(pair, None, order_side)
|
||||
|
||||
@staticmethod
|
||||
def _layout_inline_keyboard(buttons: List[InlineKeyboardButton],
|
||||
cols=3) -> List[List[InlineKeyboardButton]]:
|
||||
def _layout_inline_keyboard(
|
||||
buttons: List[InlineKeyboardButton], cols=3) -> List[List[InlineKeyboardButton]]:
|
||||
return [buttons[i:i + cols] for i in range(0, len(buttons), cols)]
|
||||
|
||||
@staticmethod
|
||||
def _layout_inline_keyboard_onecol(
|
||||
buttons: List[InlineKeyboardButton], cols=1) -> List[List[InlineKeyboardButton]]:
|
||||
return [buttons[i:i + cols] for i in range(0, len(buttons), cols)]
|
||||
|
||||
@authorized_only
|
||||
|
@ -1068,8 +1068,8 @@ def test_telegram_forcesell_handle(default_conf, update, ticker, fee,
|
||||
} == last_msg
|
||||
|
||||
|
||||
def test_telegram_forcesell_down_handle(default_conf, update, ticker, fee,
|
||||
ticker_sell_down, mocker) -> None:
|
||||
def test_telegram_force_exit_down_handle(default_conf, update, ticker, fee,
|
||||
ticker_sell_down, mocker) -> None:
|
||||
mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._find_price',
|
||||
return_value=15000.0)
|
||||
msg_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock())
|
||||
@ -1212,15 +1212,6 @@ def test_forcesell_handle_invalid(default_conf, update, mocker) -> None:
|
||||
assert msg_mock.call_count == 1
|
||||
assert 'not running' in msg_mock.call_args_list[0][0][0]
|
||||
|
||||
# No argument
|
||||
msg_mock.reset_mock()
|
||||
freqtradebot.state = State.RUNNING
|
||||
context = MagicMock()
|
||||
context.args = []
|
||||
telegram._force_exit(update=update, context=context)
|
||||
assert msg_mock.call_count == 1
|
||||
assert "You must specify a trade-id or 'all'." in msg_mock.call_args_list[0][0][0]
|
||||
|
||||
# Invalid argument
|
||||
msg_mock.reset_mock()
|
||||
freqtradebot.state = State.RUNNING
|
||||
@ -1232,6 +1223,59 @@ def test_forcesell_handle_invalid(default_conf, update, mocker) -> None:
|
||||
assert 'invalid argument' in msg_mock.call_args_list[0][0][0]
|
||||
|
||||
|
||||
def test_force_exit_no_pair(default_conf, update, ticker, fee, mocker) -> None:
|
||||
default_conf['max_open_trades'] = 4
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
fetch_ticker=ticker,
|
||||
get_fee=fee,
|
||||
_is_dry_limit_order_filled=MagicMock(return_value=True),
|
||||
)
|
||||
femock = mocker.patch('freqtrade.rpc.rpc.RPC._rpc_force_exit')
|
||||
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
|
||||
|
||||
patch_get_signal(freqtradebot)
|
||||
|
||||
# /forceexit
|
||||
context = MagicMock()
|
||||
context.args = []
|
||||
telegram._force_exit(update=update, context=context)
|
||||
# No pair
|
||||
assert msg_mock.call_args_list[0][1]['msg'] == 'No open trade found.'
|
||||
|
||||
# Create some test data
|
||||
freqtradebot.enter_positions()
|
||||
msg_mock.reset_mock()
|
||||
|
||||
# /forceexit
|
||||
telegram._force_exit(update=update, context=context)
|
||||
keyboard = msg_mock.call_args_list[0][1]['keyboard']
|
||||
# 4 pairs + cancel
|
||||
assert reduce(lambda acc, x: acc + len(x), keyboard, 0) == 5
|
||||
assert keyboard[-1][0].text == "Cancel"
|
||||
|
||||
assert keyboard[1][0].callback_data == 'force_exit__2 '
|
||||
update = MagicMock()
|
||||
update.callback_query = MagicMock()
|
||||
update.callback_query.data = keyboard[1][0].callback_data
|
||||
telegram._force_exit_inline(update, None)
|
||||
assert update.callback_query.answer.call_count == 1
|
||||
assert update.callback_query.edit_message_text.call_count == 1
|
||||
assert femock.call_count == 1
|
||||
assert femock.call_args_list[0][0][0] == '2'
|
||||
|
||||
# Retry exiting - but cancel instead
|
||||
update.callback_query.reset_mock()
|
||||
telegram._force_exit(update=update, context=context)
|
||||
# Use cancel button
|
||||
update.callback_query.data = keyboard[-1][0].callback_data
|
||||
telegram._force_exit_inline(update, None)
|
||||
query = update.callback_query
|
||||
assert query.answer.call_count == 1
|
||||
assert query.edit_message_text.call_count == 1
|
||||
assert query.edit_message_text.call_args_list[-1][1]['text'] == "Force exit canceled."
|
||||
|
||||
|
||||
def test_force_enter_handle(default_conf, update, mocker) -> None:
|
||||
mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user