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`.
|
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.
|
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]
|
### /forcelong <pair> [rate] | /forceshort <pair> [rate]
|
||||||
|
|
||||||
|
@ -103,7 +103,6 @@ class Telegram(RPCHandler):
|
|||||||
['/count', '/start', '/stop', '/help']
|
['/count', '/start', '/stop', '/help']
|
||||||
]
|
]
|
||||||
# do not allow commands with mandatory arguments and critical cmds
|
# 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
|
# TODO: DRY! - its not good to list all valid cmds here. But otherwise
|
||||||
# this needs refactoring of the whole telegram module (same
|
# this needs refactoring of the whole telegram module (same
|
||||||
# problem in _help()).
|
# problem in _help()).
|
||||||
@ -116,6 +115,7 @@ class Telegram(RPCHandler):
|
|||||||
r'/logs$', r'/whitelist$', r'/blacklist$', r'/bl_delete$',
|
r'/logs$', r'/whitelist$', r'/blacklist$', r'/bl_delete$',
|
||||||
r'/weekly$', r'/weekly \d+$', r'/monthly$', r'/monthly \d+$',
|
r'/weekly$', r'/weekly \d+$', r'/monthly$', r'/monthly \d+$',
|
||||||
r'/forcebuy$', r'/forcelong$', r'/forceshort$',
|
r'/forcebuy$', r'/forcelong$', r'/forceshort$',
|
||||||
|
r'/forcesell$', r'/forceexit$',
|
||||||
r'/edge$', r'/health$', r'/help$', r'/version$']
|
r'/edge$', r'/health$', r'/help$', r'/version$']
|
||||||
# Create keys for generation
|
# Create keys for generation
|
||||||
valid_keys_print = [k.replace('$', '') for k in valid_keys]
|
valid_keys_print = [k.replace('$', '') for k in valid_keys]
|
||||||
@ -197,7 +197,8 @@ class Telegram(RPCHandler):
|
|||||||
pattern='update_exit_reason_performance'),
|
pattern='update_exit_reason_performance'),
|
||||||
CallbackQueryHandler(self._mix_tag_performance, pattern='update_mix_tag_performance'),
|
CallbackQueryHandler(self._mix_tag_performance, pattern='update_mix_tag_performance'),
|
||||||
CallbackQueryHandler(self._count, pattern='update_count'),
|
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:
|
for handle in handles:
|
||||||
self._updater.dispatcher.add_handler(handle)
|
self._updater.dispatcher.add_handler(handle)
|
||||||
@ -928,23 +929,58 @@ class Telegram(RPCHandler):
|
|||||||
@authorized_only
|
@authorized_only
|
||||||
def _force_exit(self, update: Update, context: CallbackContext) -> None:
|
def _force_exit(self, update: Update, context: CallbackContext) -> None:
|
||||||
"""
|
"""
|
||||||
Handler for /forcesell <id>.
|
Handler for /forceexit <id>.
|
||||||
Sells the given trade at current price
|
Sells the given trade at current price
|
||||||
:param bot: telegram bot
|
:param bot: telegram bot
|
||||||
:param update: message update
|
:param update: message update
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
|
|
||||||
trade_id = context.args[0] if context.args and len(context.args) > 0 else None
|
if context.args:
|
||||||
if not trade_id:
|
trade_id = context.args[0]
|
||||||
self._send_msg("You must specify a trade-id or 'all'.")
|
self._force_exit_action(trade_id)
|
||||||
return
|
else:
|
||||||
try:
|
fiat_currency = self._config.get('fiat_display_currency', '')
|
||||||
msg = self._rpc._rpc_force_exit(trade_id)
|
try:
|
||||||
self._send_msg('Force_exit Result: `{result}`'.format(**msg))
|
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:
|
trade_buttons = [
|
||||||
self._send_msg(str(e))
|
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):
|
def _force_enter_action(self, pair, price: Optional[float], order_side: SignalDirection):
|
||||||
if pair != 'cancel':
|
if pair != 'cancel':
|
||||||
@ -964,8 +1000,13 @@ class Telegram(RPCHandler):
|
|||||||
self._force_enter_action(pair, None, order_side)
|
self._force_enter_action(pair, None, order_side)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _layout_inline_keyboard(buttons: List[InlineKeyboardButton],
|
def _layout_inline_keyboard(
|
||||||
cols=3) -> List[List[InlineKeyboardButton]]:
|
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)]
|
return [buttons[i:i + cols] for i in range(0, len(buttons), cols)]
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
|
@ -1068,8 +1068,8 @@ def test_telegram_forcesell_handle(default_conf, update, ticker, fee,
|
|||||||
} == last_msg
|
} == last_msg
|
||||||
|
|
||||||
|
|
||||||
def test_telegram_forcesell_down_handle(default_conf, update, ticker, fee,
|
def test_telegram_force_exit_down_handle(default_conf, update, ticker, fee,
|
||||||
ticker_sell_down, mocker) -> None:
|
ticker_sell_down, mocker) -> None:
|
||||||
mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._find_price',
|
mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._find_price',
|
||||||
return_value=15000.0)
|
return_value=15000.0)
|
||||||
msg_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock())
|
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 msg_mock.call_count == 1
|
||||||
assert 'not running' in msg_mock.call_args_list[0][0][0]
|
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
|
# Invalid argument
|
||||||
msg_mock.reset_mock()
|
msg_mock.reset_mock()
|
||||||
freqtradebot.state = State.RUNNING
|
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]
|
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:
|
def test_force_enter_handle(default_conf, update, mocker) -> None:
|
||||||
mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0)
|
mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0)
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user