2018-03-02 15:22:00 +00:00
|
|
|
# pragma pylint: disable=unused-argument, unused-variable, protected-access, invalid-name
|
|
|
|
|
2018-02-13 03:45:59 +00:00
|
|
|
"""
|
|
|
|
This module manage Telegram communication
|
|
|
|
"""
|
2020-06-07 08:09:39 +00:00
|
|
|
import json
|
2018-03-25 19:37:14 +00:00
|
|
|
import logging
|
2020-12-07 13:54:39 +00:00
|
|
|
from datetime import timedelta
|
2021-03-01 19:08:49 +00:00
|
|
|
from html import escape
|
2020-12-20 16:22:23 +00:00
|
|
|
from itertools import chain
|
2020-12-20 21:51:40 +00:00
|
|
|
from typing import Any, Callable, Dict, List, Union
|
2018-03-17 21:44:47 +00:00
|
|
|
|
2020-09-28 17:39:41 +00:00
|
|
|
import arrow
|
2017-11-20 21:26:32 +00:00
|
|
|
from tabulate import tabulate
|
2020-12-20 21:51:40 +00:00
|
|
|
from telegram import KeyboardButton, ParseMode, ReplyKeyboardMarkup, Update
|
2017-11-17 18:47:29 +00:00
|
|
|
from telegram.error import NetworkError, TelegramError
|
2019-12-08 13:07:46 +00:00
|
|
|
from telegram.ext import CallbackContext, CommandHandler, Updater
|
2020-08-15 18:15:02 +00:00
|
|
|
from telegram.utils.helpers import escape_markdown
|
2018-03-17 21:44:47 +00:00
|
|
|
|
2018-02-13 03:45:59 +00:00
|
|
|
from freqtrade.__init__ import __version__
|
2021-02-28 08:03:27 +00:00
|
|
|
from freqtrade.constants import DUST_PER_COIN
|
2020-12-22 11:34:21 +00:00
|
|
|
from freqtrade.exceptions import OperationalException
|
2021-02-13 15:05:56 +00:00
|
|
|
from freqtrade.misc import round_coin_value
|
2020-12-24 08:01:53 +00:00
|
|
|
from freqtrade.rpc import RPC, RPCException, RPCHandler, RPCMessageType
|
2017-11-02 17:56:57 +00:00
|
|
|
|
2020-09-28 17:39:41 +00:00
|
|
|
|
2018-03-25 19:37:14 +00:00
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
2018-06-08 21:50:38 +00:00
|
|
|
logger.debug('Included module rpc.telegram ...')
|
|
|
|
|
2019-04-10 04:59:10 +00:00
|
|
|
MAX_TELEGRAM_MESSAGE_LENGTH = 4096
|
2019-04-08 17:59:30 +00:00
|
|
|
|
|
|
|
|
2019-03-24 18:44:52 +00:00
|
|
|
def authorized_only(command_handler: Callable[..., None]) -> Callable[..., Any]:
|
2017-05-14 12:14:16 +00:00
|
|
|
"""
|
|
|
|
Decorator to check if the message comes from the correct chat_id
|
|
|
|
:param command_handler: Telegram CommandHandler
|
|
|
|
:return: decorated function
|
|
|
|
"""
|
2020-06-05 17:09:49 +00:00
|
|
|
|
2018-02-13 03:45:59 +00:00
|
|
|
def wrapper(self, *args, **kwargs):
|
2018-06-08 22:58:24 +00:00
|
|
|
""" Decorator logic """
|
2019-09-02 18:14:41 +00:00
|
|
|
update = kwargs.get('update') or args[0]
|
2017-09-08 17:25:39 +00:00
|
|
|
|
2017-11-07 21:27:16 +00:00
|
|
|
# Reject unauthorized messages
|
2018-02-13 03:45:59 +00:00
|
|
|
chat_id = int(self._config['telegram']['chat_id'])
|
|
|
|
|
2017-11-07 21:27:16 +00:00
|
|
|
if int(update.message.chat_id) != chat_id:
|
2018-03-25 19:37:14 +00:00
|
|
|
logger.info(
|
2018-02-13 03:45:59 +00:00
|
|
|
'Rejected unauthorized message from: %s',
|
|
|
|
update.message.chat_id
|
|
|
|
)
|
2017-11-07 21:27:16 +00:00
|
|
|
return wrapper
|
|
|
|
|
2018-03-25 19:37:14 +00:00
|
|
|
logger.info(
|
2018-02-13 03:45:59 +00:00
|
|
|
'Executing handler: %s for chat_id: %s',
|
|
|
|
command_handler.__name__,
|
|
|
|
chat_id
|
|
|
|
)
|
2017-11-07 21:27:16 +00:00
|
|
|
try:
|
2018-02-13 03:45:59 +00:00
|
|
|
return command_handler(self, *args, **kwargs)
|
2017-11-07 21:27:16 +00:00
|
|
|
except BaseException:
|
2018-03-25 19:37:14 +00:00
|
|
|
logger.exception('Exception occurred within Telegram module')
|
2017-09-08 13:51:00 +00:00
|
|
|
|
2018-02-13 03:45:59 +00:00
|
|
|
return wrapper
|
2017-09-08 13:51:00 +00:00
|
|
|
|
2018-03-02 15:22:00 +00:00
|
|
|
|
2020-12-24 08:01:53 +00:00
|
|
|
class Telegram(RPCHandler):
|
2018-06-08 22:58:24 +00:00
|
|
|
""" This class handles all telegram communication """
|
2018-06-08 01:49:09 +00:00
|
|
|
|
2020-12-24 08:01:53 +00:00
|
|
|
def __init__(self, rpc: RPC, config: Dict[str, Any]) -> None:
|
|
|
|
|
2018-02-13 03:45:59 +00:00
|
|
|
"""
|
2020-12-24 08:01:53 +00:00
|
|
|
Init the Telegram call, and init the super class RPCHandler
|
|
|
|
:param rpc: instance of RPC Helper class
|
|
|
|
:param config: Configuration object
|
2018-02-13 03:45:59 +00:00
|
|
|
:return: None
|
|
|
|
"""
|
2020-12-24 08:01:53 +00:00
|
|
|
super().__init__(rpc, config)
|
2018-02-13 03:45:59 +00:00
|
|
|
|
2020-12-01 18:55:20 +00:00
|
|
|
self._updater: Updater
|
2020-12-22 11:34:21 +00:00
|
|
|
self._init_keyboard()
|
2018-02-13 03:45:59 +00:00
|
|
|
self._init()
|
|
|
|
|
2020-12-22 11:34:21 +00:00
|
|
|
def _init_keyboard(self) -> None:
|
2020-12-20 21:36:56 +00:00
|
|
|
"""
|
|
|
|
Validates the keyboard configuration from telegram config
|
|
|
|
section.
|
|
|
|
"""
|
2020-12-20 21:51:40 +00:00
|
|
|
self._keyboard: List[List[Union[str, KeyboardButton]]] = [
|
2020-12-20 21:36:56 +00:00
|
|
|
['/daily', '/profit', '/balance'],
|
|
|
|
['/status', '/status table', '/performance'],
|
|
|
|
['/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 refacoring of the whole telegram module (same
|
|
|
|
# problem in _help()).
|
|
|
|
valid_keys: List[str] = ['/start', '/stop', '/status', '/status table',
|
|
|
|
'/trades', '/profit', '/performance', '/daily',
|
|
|
|
'/stats', '/count', '/locks', '/balance',
|
|
|
|
'/stopbuy', '/reload_config', '/show_config',
|
|
|
|
'/logs', '/whitelist', '/blacklist', '/edge',
|
|
|
|
'/help', '/version']
|
|
|
|
|
2020-12-22 11:34:21 +00:00
|
|
|
# custom keyboard specified in config.json
|
2020-12-20 21:36:56 +00:00
|
|
|
cust_keyboard = self._config['telegram'].get('keyboard', [])
|
|
|
|
if cust_keyboard:
|
|
|
|
# check for valid shortcuts
|
|
|
|
invalid_keys = [b for b in chain.from_iterable(cust_keyboard)
|
|
|
|
if b not in valid_keys]
|
|
|
|
if len(invalid_keys):
|
2020-12-23 15:00:01 +00:00
|
|
|
err_msg = ('config.telegram.keyboard: Invalid commands for '
|
|
|
|
f'custom Telegram keyboard: {invalid_keys}'
|
|
|
|
f'\nvalid commands are: {valid_keys}')
|
2020-12-22 11:34:21 +00:00
|
|
|
raise OperationalException(err_msg)
|
2020-12-20 21:36:56 +00:00
|
|
|
else:
|
|
|
|
self._keyboard = cust_keyboard
|
2020-12-22 11:34:21 +00:00
|
|
|
logger.info('using custom keyboard from '
|
2020-12-20 21:36:56 +00:00
|
|
|
f'config.json: {self._keyboard}')
|
|
|
|
|
2018-02-13 03:45:59 +00:00
|
|
|
def _init(self) -> None:
|
|
|
|
"""
|
|
|
|
Initializes this module with the given config,
|
|
|
|
registers all known command handlers
|
|
|
|
and starts polling for message updates
|
|
|
|
"""
|
2019-09-02 18:14:41 +00:00
|
|
|
self._updater = Updater(token=self._config['telegram']['token'], workers=0,
|
|
|
|
use_context=True)
|
2018-02-13 03:45:59 +00:00
|
|
|
|
|
|
|
# Register command handler and start telegram message polling
|
|
|
|
handles = [
|
|
|
|
CommandHandler('status', self._status),
|
|
|
|
CommandHandler('profit', self._profit),
|
|
|
|
CommandHandler('balance', self._balance),
|
|
|
|
CommandHandler('start', self._start),
|
|
|
|
CommandHandler('stop', self._stop),
|
|
|
|
CommandHandler('forcesell', self._forcesell),
|
2018-10-09 17:25:43 +00:00
|
|
|
CommandHandler('forcebuy', self._forcebuy),
|
2020-07-19 15:02:53 +00:00
|
|
|
CommandHandler('trades', self._trades),
|
2020-08-04 12:49:59 +00:00
|
|
|
CommandHandler('delete', self._delete_trade),
|
2018-02-13 03:45:59 +00:00
|
|
|
CommandHandler('performance', self._performance),
|
2020-12-09 19:26:11 +00:00
|
|
|
CommandHandler('stats', self._stats),
|
2018-02-13 03:45:59 +00:00
|
|
|
CommandHandler('daily', self._daily),
|
|
|
|
CommandHandler('count', self._count),
|
2020-10-17 13:15:35 +00:00
|
|
|
CommandHandler('locks', self._locks),
|
2021-03-01 19:08:49 +00:00
|
|
|
CommandHandler(['unlock', 'delete_locks'], self._delete_locks),
|
2020-06-10 14:55:47 +00:00
|
|
|
CommandHandler(['reload_config', 'reload_conf'], self._reload_config),
|
|
|
|
CommandHandler(['show_config', 'show_conf'], self._show_config),
|
2019-03-17 18:35:25 +00:00
|
|
|
CommandHandler('stopbuy', self._stopbuy),
|
2018-11-10 19:15:06 +00:00
|
|
|
CommandHandler('whitelist', self._whitelist),
|
2019-09-02 18:17:23 +00:00
|
|
|
CommandHandler('blacklist', self._blacklist),
|
2020-08-14 13:44:36 +00:00
|
|
|
CommandHandler('logs', self._logs),
|
2019-03-24 21:56:42 +00:00
|
|
|
CommandHandler('edge', self._edge),
|
2018-02-13 03:45:59 +00:00
|
|
|
CommandHandler('help', self._help),
|
|
|
|
CommandHandler('version', self._version),
|
|
|
|
]
|
|
|
|
for handle in handles:
|
|
|
|
self._updater.dispatcher.add_handler(handle)
|
|
|
|
self._updater.start_polling(
|
|
|
|
bootstrap_retries=-1,
|
|
|
|
timeout=30,
|
|
|
|
read_latency=60,
|
2021-04-07 04:57:05 +00:00
|
|
|
drop_pending_updates=True,
|
2018-02-13 03:45:59 +00:00
|
|
|
)
|
2018-03-25 19:37:14 +00:00
|
|
|
logger.info(
|
2018-02-13 03:45:59 +00:00
|
|
|
'rpc.telegram is listening for following commands: %s',
|
|
|
|
[h.command for h in handles]
|
|
|
|
)
|
|
|
|
|
|
|
|
def cleanup(self) -> None:
|
|
|
|
"""
|
|
|
|
Stops all running telegram threads.
|
|
|
|
:return: None
|
|
|
|
"""
|
|
|
|
self._updater.stop()
|
|
|
|
|
2021-04-20 04:41:58 +00:00
|
|
|
def _format_buy_msg(self, msg: Dict[str, Any]) -> str:
|
|
|
|
if self._rpc._fiat_converter:
|
|
|
|
msg['stake_amount_fiat'] = self._rpc._fiat_converter.convert_amount(
|
|
|
|
msg['stake_amount'], msg['stake_currency'], msg['fiat_currency'])
|
|
|
|
else:
|
|
|
|
msg['stake_amount_fiat'] = 0
|
|
|
|
|
|
|
|
message = (f"\N{LARGE BLUE CIRCLE} *{msg['exchange']}:* Buying {msg['pair']}"
|
|
|
|
f" (#{msg['trade_id']})\n"
|
|
|
|
f"*Amount:* `{msg['amount']:.8f}`\n"
|
|
|
|
f"*Open Rate:* `{msg['limit']:.8f}`\n"
|
|
|
|
f"*Current Rate:* `{msg['current_rate']:.8f}`\n"
|
|
|
|
f"*Total:* `({round_coin_value(msg['stake_amount'], msg['stake_currency'])}")
|
|
|
|
|
|
|
|
if msg.get('fiat_currency', None):
|
|
|
|
message += f", {round_coin_value(msg['stake_amount_fiat'], msg['fiat_currency'])}"
|
|
|
|
message += ")`"
|
|
|
|
return message
|
|
|
|
|
|
|
|
def _format_sell_msg(self, msg: Dict[str, Any]) -> str:
|
|
|
|
msg['amount'] = round(msg['amount'], 8)
|
|
|
|
msg['profit_percent'] = round(msg['profit_ratio'] * 100, 2)
|
|
|
|
msg['duration'] = msg['close_date'].replace(
|
|
|
|
microsecond=0) - msg['open_date'].replace(microsecond=0)
|
|
|
|
msg['duration_min'] = msg['duration'].total_seconds() / 60
|
|
|
|
|
|
|
|
msg['emoji'] = self._get_sell_emoji(msg)
|
|
|
|
|
|
|
|
message = ("{emoji} *{exchange}:* Selling {pair} (#{trade_id})\n"
|
|
|
|
"*Amount:* `{amount:.8f}`\n"
|
|
|
|
"*Open Rate:* `{open_rate:.8f}`\n"
|
|
|
|
"*Current Rate:* `{current_rate:.8f}`\n"
|
|
|
|
"*Close Rate:* `{limit:.8f}`\n"
|
|
|
|
"*Sell Reason:* `{sell_reason}`\n"
|
|
|
|
"*Duration:* `{duration} ({duration_min:.1f} min)`\n"
|
|
|
|
"*Profit:* `{profit_percent:.2f}%`").format(**msg)
|
|
|
|
|
|
|
|
# Check if all sell properties are available.
|
|
|
|
# This might not be the case if the message origin is triggered by /forcesell
|
|
|
|
if (all(prop in msg for prop in ['gain', 'fiat_currency', 'stake_currency'])
|
|
|
|
and self._rpc._fiat_converter):
|
|
|
|
msg['profit_fiat'] = self._rpc._fiat_converter.convert_amount(
|
|
|
|
msg['profit_amount'], msg['stake_currency'], msg['fiat_currency'])
|
|
|
|
message += (' `({gain}: {profit_amount:.8f} {stake_currency}'
|
|
|
|
' / {profit_fiat:.3f} {fiat_currency})`').format(**msg)
|
|
|
|
return message
|
|
|
|
|
2018-07-03 18:26:48 +00:00
|
|
|
def send_msg(self, msg: Dict[str, Any]) -> None:
|
2018-06-08 01:49:09 +00:00
|
|
|
""" Send a message to telegram channel """
|
2018-07-03 18:26:48 +00:00
|
|
|
|
2020-09-19 17:38:33 +00:00
|
|
|
noti = self._config['telegram'].get('notification_settings', {}
|
2020-09-19 18:38:42 +00:00
|
|
|
).get(str(msg['type']), 'on')
|
2020-09-19 17:38:33 +00:00
|
|
|
if noti == 'off':
|
2020-09-19 18:04:12 +00:00
|
|
|
logger.info(f"Notification '{msg['type']}' not sent.")
|
2020-09-19 17:38:33 +00:00
|
|
|
# Notification disabled
|
|
|
|
return
|
|
|
|
|
2021-04-20 04:41:58 +00:00
|
|
|
if msg['type'] == RPCMessageType.BUY:
|
|
|
|
message = self._format_buy_msg(msg)
|
2018-07-24 07:20:32 +00:00
|
|
|
|
2021-04-20 04:41:58 +00:00
|
|
|
elif msg['type'] in (RPCMessageType.BUY_CANCEL, RPCMessageType.SELL_CANCEL):
|
|
|
|
msg['message_side'] = 'buy' if msg['type'] == RPCMessageType.BUY_CANCEL else 'sell'
|
2020-06-06 15:28:00 +00:00
|
|
|
message = ("\N{WARNING SIGN} *{exchange}:* "
|
2021-04-20 04:41:58 +00:00
|
|
|
"Cancelling open {message_side} Order for {pair} (#{trade_id}). "
|
2021-03-06 13:07:47 +00:00
|
|
|
"Reason: {reason}.".format(**msg))
|
2020-02-08 20:02:52 +00:00
|
|
|
|
2021-04-20 17:36:30 +00:00
|
|
|
elif msg['type'] in (RPCMessageType.BUY_FILL, RPCMessageType.SELL_FILL):
|
2021-04-20 04:41:58 +00:00
|
|
|
msg['message_side'] = 'Buy' if msg['type'] == RPCMessageType.BUY_FILL else 'Sell'
|
|
|
|
|
2021-04-19 17:58:29 +00:00
|
|
|
message = ("\N{LARGE CIRCLE} *{exchange}:* "
|
|
|
|
"Buy order for {pair} (#{trade_id}) filled for {open_rate}.".format(**msg))
|
|
|
|
|
2021-04-20 04:41:58 +00:00
|
|
|
elif msg['type'] == RPCMessageType.SELL:
|
|
|
|
message = self._format_sell_msg(msg)
|
2021-04-19 17:58:29 +00:00
|
|
|
|
2021-04-20 04:41:58 +00:00
|
|
|
elif msg['type'] == RPCMessageType.STATUS:
|
2018-07-03 18:26:48 +00:00
|
|
|
message = '*Status:* `{status}`'.format(**msg)
|
|
|
|
|
2021-04-20 04:41:58 +00:00
|
|
|
elif msg['type'] == RPCMessageType.WARNING:
|
2020-06-05 17:09:49 +00:00
|
|
|
message = '\N{WARNING SIGN} *Warning:* `{status}`'.format(**msg)
|
2018-08-15 02:39:32 +00:00
|
|
|
|
2021-04-20 04:41:58 +00:00
|
|
|
elif msg['type'] == RPCMessageType.STARTUP:
|
2018-08-15 02:39:32 +00:00
|
|
|
message = '{status}'.format(**msg)
|
|
|
|
|
2018-07-03 18:26:48 +00:00
|
|
|
else:
|
|
|
|
raise NotImplementedError('Unknown message type: {}'.format(msg['type']))
|
|
|
|
|
2020-09-19 17:38:33 +00:00
|
|
|
self._send_msg(message, disable_notification=(noti == 'silent'))
|
2018-06-08 01:49:09 +00:00
|
|
|
|
2020-06-06 13:38:42 +00:00
|
|
|
def _get_sell_emoji(self, msg):
|
2020-06-06 11:32:06 +00:00
|
|
|
"""
|
|
|
|
Get emoji for sell-side
|
|
|
|
"""
|
|
|
|
|
|
|
|
if float(msg['profit_percent']) >= 5.0:
|
|
|
|
return "\N{ROCKET}"
|
|
|
|
elif float(msg['profit_percent']) >= 0.0:
|
|
|
|
return "\N{EIGHT SPOKED ASTERISK}"
|
|
|
|
elif msg['sell_reason'] == "stop_loss":
|
|
|
|
return"\N{WARNING SIGN}"
|
|
|
|
else:
|
|
|
|
return "\N{CROSS MARK}"
|
|
|
|
|
2018-02-13 03:45:59 +00:00
|
|
|
@authorized_only
|
2019-09-02 18:14:41 +00:00
|
|
|
def _status(self, update: Update, context: CallbackContext) -> None:
|
2018-02-13 03:45:59 +00:00
|
|
|
"""
|
|
|
|
Handler for /status.
|
|
|
|
Returns the current TradeThread status
|
|
|
|
:param bot: telegram bot
|
|
|
|
:param update: message update
|
|
|
|
:return: None
|
|
|
|
"""
|
|
|
|
|
2020-12-01 18:55:20 +00:00
|
|
|
if context.args and 'table' in context.args:
|
2019-09-02 18:17:23 +00:00
|
|
|
self._status_table(update, context)
|
2018-02-13 03:45:59 +00:00
|
|
|
return
|
|
|
|
|
2018-06-08 02:52:50 +00:00
|
|
|
try:
|
2021-01-17 19:39:35 +00:00
|
|
|
|
2021-01-17 20:26:55 +00:00
|
|
|
# Check if there's at least one numerical ID provided.
|
|
|
|
# If so, try to get only these trades.
|
2021-01-18 14:26:53 +00:00
|
|
|
trade_ids = []
|
2021-01-17 19:39:35 +00:00
|
|
|
if context.args and len(context.args) > 0:
|
2021-01-18 14:26:53 +00:00
|
|
|
trade_ids = [int(i) for i in context.args if i.isnumeric()]
|
2021-01-17 19:39:35 +00:00
|
|
|
|
|
|
|
results = self._rpc._rpc_trade_status(trade_ids=trade_ids)
|
2018-07-12 15:27:40 +00:00
|
|
|
|
2019-03-27 20:12:57 +00:00
|
|
|
messages = []
|
|
|
|
for r in results:
|
2021-04-13 04:17:11 +00:00
|
|
|
r['open_date_hum'] = arrow.get(r['open_date']).humanize()
|
2019-03-27 20:12:57 +00:00
|
|
|
lines = [
|
2019-05-06 04:55:12 +00:00
|
|
|
"*Trade ID:* `{trade_id}` `(since {open_date_hum})`",
|
2019-03-27 20:12:57 +00:00
|
|
|
"*Current Pair:* {pair}",
|
2019-04-03 14:28:44 +00:00
|
|
|
"*Amount:* `{amount} ({stake_amount} {base_currency})`",
|
2019-03-27 20:12:57 +00:00
|
|
|
"*Open Rate:* `{open_rate:.8f}`",
|
|
|
|
"*Close Rate:* `{close_rate}`" if r['close_rate'] else "",
|
|
|
|
"*Current Rate:* `{current_rate:.8f}`",
|
2020-11-03 07:34:12 +00:00
|
|
|
("*Current Profit:* " if r['is_open'] else "*Close Profit: *")
|
|
|
|
+ "`{profit_pct:.2f}%`",
|
2019-03-27 20:12:57 +00:00
|
|
|
]
|
2020-11-03 07:25:47 +00:00
|
|
|
if (r['stop_loss_abs'] != r['initial_stop_loss_abs']
|
2020-07-14 18:16:18 +00:00
|
|
|
and r['initial_stop_loss_pct'] is not None):
|
|
|
|
# Adding initial stoploss only if it is different from stoploss
|
2020-11-03 07:34:12 +00:00
|
|
|
lines.append("*Initial Stoploss:* `{initial_stop_loss_abs:.8f}` "
|
2020-07-14 18:16:18 +00:00
|
|
|
"`({initial_stop_loss_pct:.2f}%)`")
|
|
|
|
|
|
|
|
# Adding stoploss and stoploss percentage only if it is not None
|
2020-11-03 07:25:47 +00:00
|
|
|
lines.append("*Stoploss:* `{stop_loss_abs:.8f}` " +
|
2020-07-14 18:16:18 +00:00
|
|
|
("`({stop_loss_pct:.2f}%)`" if r['stop_loss_pct'] else ""))
|
|
|
|
lines.append("*Stoploss distance:* `{stoploss_current_dist:.8f}` "
|
|
|
|
"`({stoploss_current_dist_pct:.2f}%)`")
|
2020-05-17 08:52:20 +00:00
|
|
|
if r['open_order']:
|
|
|
|
if r['sell_order_status']:
|
|
|
|
lines.append("*Open Order:* `{open_order}` - `{sell_order_status}`")
|
|
|
|
else:
|
|
|
|
lines.append("*Open Order:* `{open_order}`")
|
|
|
|
|
2019-07-14 18:14:35 +00:00
|
|
|
# Filter empty lines using list-comprehension
|
2020-05-18 09:40:25 +00:00
|
|
|
messages.append("\n".join([line for line in lines if line]).format(**r))
|
2019-03-27 20:12:57 +00:00
|
|
|
|
2018-06-22 01:54:10 +00:00
|
|
|
for msg in messages:
|
2019-09-02 18:14:41 +00:00
|
|
|
self._send_msg(msg)
|
2019-03-27 20:12:57 +00:00
|
|
|
|
2018-06-08 02:52:50 +00:00
|
|
|
except RPCException as e:
|
2019-09-02 18:14:41 +00:00
|
|
|
self._send_msg(str(e))
|
2018-02-13 03:45:59 +00:00
|
|
|
|
|
|
|
@authorized_only
|
2019-09-02 18:14:41 +00:00
|
|
|
def _status_table(self, update: Update, context: CallbackContext) -> None:
|
2018-02-13 03:45:59 +00:00
|
|
|
"""
|
|
|
|
Handler for /status table.
|
|
|
|
Returns the current TradeThread status in table format
|
|
|
|
:param bot: telegram bot
|
|
|
|
:param update: message update
|
|
|
|
:return: None
|
|
|
|
"""
|
2018-06-08 02:52:50 +00:00
|
|
|
try:
|
2020-12-24 08:01:53 +00:00
|
|
|
statlist, head = self._rpc._rpc_status_table(
|
|
|
|
self._config['stake_currency'], self._config.get('fiat_display_currency', ''))
|
|
|
|
|
2021-03-22 19:40:11 +00:00
|
|
|
max_trades_per_msg = 50
|
2021-03-23 20:52:46 +00:00
|
|
|
"""
|
|
|
|
Calculate the number of messages of 50 trades per message
|
|
|
|
0.99 is used to make sure that there are no extra (empty) messages
|
|
|
|
As an example with 50 trades, there will be int(50/50 + 0.99) = 1 message
|
|
|
|
"""
|
2021-03-23 15:54:38 +00:00
|
|
|
for i in range(0, max(int(len(statlist) / max_trades_per_msg + 0.99), 1)):
|
2021-03-22 19:40:11 +00:00
|
|
|
message = tabulate(statlist[i * max_trades_per_msg:(i + 1) * max_trades_per_msg],
|
|
|
|
headers=head,
|
|
|
|
tablefmt='simple')
|
|
|
|
self._send_msg(f"<pre>{message}</pre>", parse_mode=ParseMode.HTML)
|
2018-06-08 02:52:50 +00:00
|
|
|
except RPCException as e:
|
2019-09-02 18:14:41 +00:00
|
|
|
self._send_msg(str(e))
|
2018-02-13 03:45:59 +00:00
|
|
|
|
|
|
|
@authorized_only
|
2019-09-02 18:14:41 +00:00
|
|
|
def _daily(self, update: Update, context: CallbackContext) -> None:
|
2018-02-13 03:45:59 +00:00
|
|
|
"""
|
|
|
|
Handler for /daily <n>
|
|
|
|
Returns a daily profit (in BTC) over the last n days.
|
|
|
|
:param bot: telegram bot
|
|
|
|
:param update: message update
|
|
|
|
:return: None
|
|
|
|
"""
|
2018-06-23 22:17:10 +00:00
|
|
|
stake_cur = self._config['stake_currency']
|
2018-07-26 06:26:23 +00:00
|
|
|
fiat_disp_cur = self._config.get('fiat_display_currency', '')
|
2018-02-13 03:45:59 +00:00
|
|
|
try:
|
2020-12-05 07:16:40 +00:00
|
|
|
timescale = int(context.args[0]) if context.args else 7
|
2019-09-02 18:17:23 +00:00
|
|
|
except (TypeError, ValueError, IndexError):
|
2018-02-13 03:45:59 +00:00
|
|
|
timescale = 7
|
2018-06-08 02:52:50 +00:00
|
|
|
try:
|
2020-12-24 08:01:53 +00:00
|
|
|
stats = self._rpc._rpc_daily_profit(
|
2018-06-08 02:52:50 +00:00
|
|
|
timescale,
|
2018-06-23 22:17:10 +00:00
|
|
|
stake_cur,
|
|
|
|
fiat_disp_cur
|
2018-06-08 02:52:50 +00:00
|
|
|
)
|
2020-05-17 18:12:01 +00:00
|
|
|
stats_tab = tabulate(
|
|
|
|
[[day['date'],
|
2021-02-13 15:05:56 +00:00
|
|
|
f"{round_coin_value(day['abs_profit'], stats['stake_currency'])}",
|
2020-08-18 18:12:14 +00:00
|
|
|
f"{day['fiat_value']:.3f} {stats['fiat_display_currency']}",
|
2020-05-17 18:12:01 +00:00
|
|
|
f"{day['trade_count']} trades"] for day in stats['data']],
|
|
|
|
headers=[
|
|
|
|
'Day',
|
|
|
|
f'Profit {stake_cur}',
|
|
|
|
f'Profit {fiat_disp_cur}',
|
2020-05-18 09:40:25 +00:00
|
|
|
'Trades',
|
2020-05-17 18:12:01 +00:00
|
|
|
],
|
|
|
|
tablefmt='simple')
|
2019-01-17 19:28:21 +00:00
|
|
|
message = f'<b>Daily Profit over the last {timescale} days</b>:\n<pre>{stats_tab}</pre>'
|
2019-09-02 18:14:41 +00:00
|
|
|
self._send_msg(message, parse_mode=ParseMode.HTML)
|
2018-06-08 02:52:50 +00:00
|
|
|
except RPCException as e:
|
2019-09-02 18:14:41 +00:00
|
|
|
self._send_msg(str(e))
|
2018-02-13 03:45:59 +00:00
|
|
|
|
|
|
|
@authorized_only
|
2019-09-02 18:14:41 +00:00
|
|
|
def _profit(self, update: Update, context: CallbackContext) -> None:
|
2018-02-13 03:45:59 +00:00
|
|
|
"""
|
|
|
|
Handler for /profit.
|
|
|
|
Returns a cumulative profit statistics.
|
|
|
|
:param bot: telegram bot
|
|
|
|
:param update: message update
|
|
|
|
:return: None
|
|
|
|
"""
|
2018-06-23 22:17:10 +00:00
|
|
|
stake_cur = self._config['stake_currency']
|
2018-07-26 06:26:23 +00:00
|
|
|
fiat_disp_cur = self._config.get('fiat_display_currency', '')
|
2018-06-23 22:17:10 +00:00
|
|
|
|
2020-12-24 08:01:53 +00:00
|
|
|
stats = self._rpc._rpc_trade_statistics(
|
2020-05-29 08:10:45 +00:00
|
|
|
stake_cur,
|
|
|
|
fiat_disp_cur)
|
|
|
|
profit_closed_coin = stats['profit_closed_coin']
|
2020-06-03 17:40:30 +00:00
|
|
|
profit_closed_percent_mean = stats['profit_closed_percent_mean']
|
|
|
|
profit_closed_percent_sum = stats['profit_closed_percent_sum']
|
2020-05-29 08:10:45 +00:00
|
|
|
profit_closed_fiat = stats['profit_closed_fiat']
|
|
|
|
profit_all_coin = stats['profit_all_coin']
|
2020-06-03 17:40:30 +00:00
|
|
|
profit_all_percent_mean = stats['profit_all_percent_mean']
|
|
|
|
profit_all_percent_sum = stats['profit_all_percent_sum']
|
2020-05-29 08:10:45 +00:00
|
|
|
profit_all_fiat = stats['profit_all_fiat']
|
|
|
|
trade_count = stats['trade_count']
|
|
|
|
first_trade_date = stats['first_trade_date']
|
|
|
|
latest_trade_date = stats['latest_trade_date']
|
|
|
|
avg_duration = stats['avg_duration']
|
|
|
|
best_pair = stats['best_pair']
|
|
|
|
best_rate = stats['best_rate']
|
2020-05-30 17:42:09 +00:00
|
|
|
if stats['trade_count'] == 0:
|
|
|
|
markdown_msg = 'No trades yet.'
|
2020-05-29 08:10:45 +00:00
|
|
|
else:
|
2020-05-30 17:42:09 +00:00
|
|
|
# Message to display
|
|
|
|
if stats['closed_trade_count'] > 0:
|
|
|
|
markdown_msg = ("*ROI:* Closed trades\n"
|
2021-02-13 15:05:56 +00:00
|
|
|
f"∙ `{round_coin_value(profit_closed_coin, stake_cur)} "
|
2020-06-03 17:40:30 +00:00
|
|
|
f"({profit_closed_percent_mean:.2f}%) "
|
|
|
|
f"({profit_closed_percent_sum} \N{GREEK CAPITAL LETTER SIGMA}%)`\n"
|
2021-02-13 15:05:56 +00:00
|
|
|
f"∙ `{round_coin_value(profit_closed_fiat, fiat_disp_cur)}`\n")
|
2020-05-30 17:42:09 +00:00
|
|
|
else:
|
|
|
|
markdown_msg = "`No closed trade` \n"
|
|
|
|
|
|
|
|
markdown_msg += (f"*ROI:* All trades\n"
|
2021-02-13 15:05:56 +00:00
|
|
|
f"∙ `{round_coin_value(profit_all_coin, stake_cur)} "
|
2020-06-03 17:40:30 +00:00
|
|
|
f"({profit_all_percent_mean:.2f}%) "
|
|
|
|
f"({profit_all_percent_sum} \N{GREEK CAPITAL LETTER SIGMA}%)`\n"
|
2021-02-13 15:05:56 +00:00
|
|
|
f"∙ `{round_coin_value(profit_all_fiat, fiat_disp_cur)}`\n"
|
2020-05-30 17:42:09 +00:00
|
|
|
f"*Total Trade Count:* `{trade_count}`\n"
|
|
|
|
f"*First Trade opened:* `{first_trade_date}`\n"
|
2020-06-24 04:43:19 +00:00
|
|
|
f"*Latest Trade opened:* `{latest_trade_date}\n`"
|
|
|
|
f"*Win / Loss:* `{stats['winning_trades']} / {stats['losing_trades']}`"
|
|
|
|
)
|
2020-05-30 17:42:09 +00:00
|
|
|
if stats['closed_trade_count'] > 0:
|
|
|
|
markdown_msg += (f"\n*Avg. Duration:* `{avg_duration}`\n"
|
|
|
|
f"*Best Performing:* `{best_pair}: {best_rate:.2f}%`")
|
2020-05-29 08:10:45 +00:00
|
|
|
self._send_msg(markdown_msg)
|
2018-02-13 03:45:59 +00:00
|
|
|
|
2020-12-05 13:39:50 +00:00
|
|
|
@authorized_only
|
|
|
|
def _stats(self, update: Update, context: CallbackContext) -> None:
|
|
|
|
"""
|
|
|
|
Handler for /stats
|
|
|
|
Show stats of recent trades
|
|
|
|
"""
|
2020-12-24 08:01:53 +00:00
|
|
|
stats = self._rpc._rpc_stats()
|
2020-12-05 13:39:50 +00:00
|
|
|
|
|
|
|
reason_map = {
|
|
|
|
'roi': 'ROI',
|
|
|
|
'stop_loss': 'Stoploss',
|
|
|
|
'trailing_stop_loss': 'Trail. Stop',
|
|
|
|
'stoploss_on_exchange': 'Stoploss',
|
|
|
|
'sell_signal': 'Sell Signal',
|
|
|
|
'force_sell': 'Forcesell',
|
|
|
|
'emergency_sell': 'Emergency Sell',
|
|
|
|
}
|
2020-12-10 06:39:50 +00:00
|
|
|
sell_reasons_tabulate = [
|
|
|
|
[
|
2020-12-05 13:39:50 +00:00
|
|
|
reason_map.get(reason, reason),
|
|
|
|
sum(count.values()),
|
2020-12-07 13:54:39 +00:00
|
|
|
count['wins'],
|
|
|
|
count['losses']
|
2020-12-10 06:39:50 +00:00
|
|
|
] for reason, count in stats['sell_reasons'].items()
|
|
|
|
]
|
2020-12-05 13:39:50 +00:00
|
|
|
sell_reasons_msg = tabulate(
|
|
|
|
sell_reasons_tabulate,
|
|
|
|
headers=['Sell Reason', 'Sells', 'Wins', 'Losses']
|
|
|
|
)
|
2020-12-07 13:54:39 +00:00
|
|
|
durations = stats['durations']
|
2020-12-05 13:39:50 +00:00
|
|
|
duration_msg = tabulate([
|
|
|
|
['Wins', str(timedelta(seconds=durations['wins']))
|
|
|
|
if durations['wins'] != 'N/A' else 'N/A'],
|
|
|
|
['Losses', str(timedelta(seconds=durations['losses']))
|
|
|
|
if durations['losses'] != 'N/A' else 'N/A']
|
2020-12-07 13:54:39 +00:00
|
|
|
],
|
2020-12-05 13:39:50 +00:00
|
|
|
headers=['', 'Avg. Duration']
|
|
|
|
)
|
|
|
|
msg = (f"""```\n{sell_reasons_msg}```\n```\n{duration_msg}```""")
|
|
|
|
|
|
|
|
self._send_msg(msg, ParseMode.MARKDOWN)
|
|
|
|
|
2018-02-13 03:45:59 +00:00
|
|
|
@authorized_only
|
2019-09-02 18:14:41 +00:00
|
|
|
def _balance(self, update: Update, context: CallbackContext) -> None:
|
2018-06-08 22:58:24 +00:00
|
|
|
""" Handler for /balance """
|
2018-06-08 02:52:50 +00:00
|
|
|
try:
|
2020-12-24 08:01:53 +00:00
|
|
|
result = self._rpc._rpc_balance(self._config['stake_currency'],
|
|
|
|
self._config.get('fiat_display_currency', ''))
|
2019-12-15 08:38:18 +00:00
|
|
|
|
2021-02-28 08:03:27 +00:00
|
|
|
balance_dust_level = self._config['telegram'].get('balance_dust_level', 0.0)
|
|
|
|
if not balance_dust_level:
|
|
|
|
balance_dust_level = DUST_PER_COIN.get(self._config['stake_currency'], 1.0)
|
2021-02-17 22:09:39 +00:00
|
|
|
|
2018-06-08 02:52:50 +00:00
|
|
|
output = ''
|
2019-12-15 08:38:18 +00:00
|
|
|
if self._config['dry_run']:
|
|
|
|
output += (
|
2019-12-24 05:58:30 +00:00
|
|
|
f"*Warning:* Simulated balances in Dry Mode.\n"
|
2019-12-15 09:41:57 +00:00
|
|
|
"This mode is still experimental!\n"
|
|
|
|
"Starting capital: "
|
2019-12-15 08:38:18 +00:00
|
|
|
f"`{self._config['dry_run_wallet']}` {self._config['stake_currency']}.\n"
|
2020-06-05 17:09:49 +00:00
|
|
|
)
|
2021-02-13 15:05:56 +00:00
|
|
|
for curr in result['currencies']:
|
2021-02-17 22:09:39 +00:00
|
|
|
if curr['est_stake'] > balance_dust_level:
|
2021-02-13 15:05:56 +00:00
|
|
|
curr_output = (
|
|
|
|
f"*{curr['currency']}:*\n"
|
|
|
|
f"\t`Available: {curr['free']:.8f}`\n"
|
|
|
|
f"\t`Balance: {curr['balance']:.8f}`\n"
|
|
|
|
f"\t`Pending: {curr['used']:.8f}`\n"
|
|
|
|
f"\t`Est. {curr['stake']}: "
|
|
|
|
f"{round_coin_value(curr['est_stake'], curr['stake'], False)}`\n")
|
2018-10-10 19:29:40 +00:00
|
|
|
else:
|
2021-02-28 08:03:27 +00:00
|
|
|
curr_output = (f"*{curr['currency']}:* not showing <{balance_dust_level} "
|
|
|
|
f"{curr['stake']} amount \n")
|
2019-04-08 17:59:30 +00:00
|
|
|
|
|
|
|
# Handle overflowing messsage length
|
2019-04-10 04:59:10 +00:00
|
|
|
if len(output + curr_output) >= MAX_TELEGRAM_MESSAGE_LENGTH:
|
2019-09-02 18:14:41 +00:00
|
|
|
self._send_msg(output)
|
2019-04-08 17:59:30 +00:00
|
|
|
output = curr_output
|
|
|
|
else:
|
|
|
|
output += curr_output
|
2018-06-08 02:52:50 +00:00
|
|
|
|
2020-06-06 15:28:00 +00:00
|
|
|
output += ("\n*Estimated Value*:\n"
|
2021-02-13 15:05:56 +00:00
|
|
|
f"\t`{result['stake']}: {result['total']: .8f}`\n"
|
|
|
|
f"\t`{result['symbol']}: "
|
|
|
|
f"{round_coin_value(result['value'], result['symbol'], False)}`\n")
|
2019-09-02 18:14:41 +00:00
|
|
|
self._send_msg(output)
|
2018-06-08 02:52:50 +00:00
|
|
|
except RPCException as e:
|
2019-09-02 18:14:41 +00:00
|
|
|
self._send_msg(str(e))
|
2018-02-13 03:45:59 +00:00
|
|
|
|
|
|
|
@authorized_only
|
2019-09-02 18:14:41 +00:00
|
|
|
def _start(self, update: Update, context: CallbackContext) -> None:
|
2018-02-13 03:45:59 +00:00
|
|
|
"""
|
|
|
|
Handler for /start.
|
|
|
|
Starts TradeThread
|
|
|
|
:param bot: telegram bot
|
|
|
|
:param update: message update
|
|
|
|
:return: None
|
|
|
|
"""
|
2020-12-24 08:01:53 +00:00
|
|
|
msg = self._rpc._rpc_start()
|
2019-09-02 18:14:41 +00:00
|
|
|
self._send_msg('Status: `{status}`'.format(**msg))
|
2018-02-13 03:45:59 +00:00
|
|
|
|
|
|
|
@authorized_only
|
2019-09-02 18:14:41 +00:00
|
|
|
def _stop(self, update: Update, context: CallbackContext) -> None:
|
2018-02-13 03:45:59 +00:00
|
|
|
"""
|
|
|
|
Handler for /stop.
|
|
|
|
Stops TradeThread
|
|
|
|
:param bot: telegram bot
|
|
|
|
:param update: message update
|
|
|
|
:return: None
|
|
|
|
"""
|
2020-12-24 08:01:53 +00:00
|
|
|
msg = self._rpc._rpc_stop()
|
2019-09-02 18:14:41 +00:00
|
|
|
self._send_msg('Status: `{status}`'.format(**msg))
|
2018-02-13 03:45:59 +00:00
|
|
|
|
2018-06-09 02:29:48 +00:00
|
|
|
@authorized_only
|
2020-06-09 21:03:55 +00:00
|
|
|
def _reload_config(self, update: Update, context: CallbackContext) -> None:
|
2018-06-09 02:29:48 +00:00
|
|
|
"""
|
2020-06-09 21:03:55 +00:00
|
|
|
Handler for /reload_config.
|
2018-06-09 02:29:48 +00:00
|
|
|
Triggers a config file reload
|
|
|
|
:param bot: telegram bot
|
|
|
|
:param update: message update
|
|
|
|
:return: None
|
|
|
|
"""
|
2020-12-24 08:01:53 +00:00
|
|
|
msg = self._rpc._rpc_reload_config()
|
2019-09-02 18:14:41 +00:00
|
|
|
self._send_msg('Status: `{status}`'.format(**msg))
|
2018-06-09 02:29:48 +00:00
|
|
|
|
2019-03-17 18:35:25 +00:00
|
|
|
@authorized_only
|
2019-09-02 18:14:41 +00:00
|
|
|
def _stopbuy(self, update: Update, context: CallbackContext) -> None:
|
2019-03-17 18:35:25 +00:00
|
|
|
"""
|
|
|
|
Handler for /stop_buy.
|
|
|
|
Sets max_open_trades to 0 and gracefully sells all open trades
|
|
|
|
:param bot: telegram bot
|
|
|
|
:param update: message update
|
|
|
|
:return: None
|
|
|
|
"""
|
2020-12-24 08:01:53 +00:00
|
|
|
msg = self._rpc._rpc_stopbuy()
|
2019-09-02 18:14:41 +00:00
|
|
|
self._send_msg('Status: `{status}`'.format(**msg))
|
2019-03-17 18:35:25 +00:00
|
|
|
|
2018-02-13 03:45:59 +00:00
|
|
|
@authorized_only
|
2019-09-02 18:14:41 +00:00
|
|
|
def _forcesell(self, update: Update, context: CallbackContext) -> None:
|
2018-02-13 03:45:59 +00:00
|
|
|
"""
|
|
|
|
Handler for /forcesell <id>.
|
|
|
|
Sells the given trade at current price
|
|
|
|
:param bot: telegram bot
|
|
|
|
:param update: message update
|
|
|
|
:return: None
|
|
|
|
"""
|
|
|
|
|
2020-12-01 18:55:20 +00:00
|
|
|
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
|
2018-06-08 02:52:50 +00:00
|
|
|
try:
|
2020-12-24 08:01:53 +00:00
|
|
|
msg = self._rpc._rpc_forcesell(trade_id)
|
2019-09-02 18:14:41 +00:00
|
|
|
self._send_msg('Forcesell Result: `{result}`'.format(**msg))
|
2019-04-30 04:23:14 +00:00
|
|
|
|
2018-06-08 02:52:50 +00:00
|
|
|
except RPCException as e:
|
2019-09-02 18:14:41 +00:00
|
|
|
self._send_msg(str(e))
|
2018-02-13 03:45:59 +00:00
|
|
|
|
2018-10-09 17:25:43 +00:00
|
|
|
@authorized_only
|
2019-09-02 18:14:41 +00:00
|
|
|
def _forcebuy(self, update: Update, context: CallbackContext) -> None:
|
2018-10-09 17:25:43 +00:00
|
|
|
"""
|
|
|
|
Handler for /forcebuy <asset> <price>.
|
|
|
|
Buys a pair trade at the given or current price
|
|
|
|
:param bot: telegram bot
|
|
|
|
:param update: message update
|
|
|
|
:return: None
|
|
|
|
"""
|
2020-12-01 18:55:20 +00:00
|
|
|
if context.args:
|
|
|
|
pair = context.args[0]
|
|
|
|
price = float(context.args[1]) if len(context.args) > 1 else None
|
|
|
|
try:
|
2020-12-24 08:01:53 +00:00
|
|
|
self._rpc._rpc_forcebuy(pair, price)
|
2020-12-01 18:55:20 +00:00
|
|
|
except RPCException as e:
|
|
|
|
self._send_msg(str(e))
|
2018-10-09 17:25:43 +00:00
|
|
|
|
2020-07-19 15:02:53 +00:00
|
|
|
@authorized_only
|
|
|
|
def _trades(self, update: Update, context: CallbackContext) -> None:
|
|
|
|
"""
|
|
|
|
Handler for /trades <n>
|
|
|
|
Returns last n recent trades.
|
|
|
|
:param bot: telegram bot
|
|
|
|
:param update: message update
|
|
|
|
:return: None
|
|
|
|
"""
|
2020-07-23 02:36:05 +00:00
|
|
|
stake_cur = self._config['stake_currency']
|
2020-07-19 15:02:53 +00:00
|
|
|
try:
|
2020-12-01 18:55:20 +00:00
|
|
|
nrecent = int(context.args[0]) if context.args else 10
|
2020-07-19 15:02:53 +00:00
|
|
|
except (TypeError, ValueError, IndexError):
|
|
|
|
nrecent = 10
|
|
|
|
try:
|
2020-12-24 08:01:53 +00:00
|
|
|
trades = self._rpc._rpc_trade_history(
|
2020-07-19 15:02:53 +00:00
|
|
|
nrecent
|
|
|
|
)
|
|
|
|
trades_tab = tabulate(
|
2021-03-08 21:21:56 +00:00
|
|
|
[[arrow.get(trade['close_date']).humanize(),
|
|
|
|
trade['pair'] + " (#" + str(trade['trade_id']) + ")",
|
2020-07-23 02:47:53 +00:00
|
|
|
f"{(100 * trade['close_profit']):.2f}% ({trade['close_profit_abs']})"]
|
2020-07-23 05:50:45 +00:00
|
|
|
for trade in trades['trades']],
|
2020-07-19 15:02:53 +00:00
|
|
|
headers=[
|
2021-03-08 21:21:56 +00:00
|
|
|
'Close Date',
|
|
|
|
'Pair (ID)',
|
2020-07-23 02:36:05 +00:00
|
|
|
f'Profit ({stake_cur})',
|
2020-07-19 15:02:53 +00:00
|
|
|
],
|
|
|
|
tablefmt='simple')
|
2020-07-23 05:12:14 +00:00
|
|
|
message = (f"<b>{min(trades['trades_count'], nrecent)} recent trades</b>:\n"
|
|
|
|
+ (f"<pre>{trades_tab}</pre>" if trades['trades_count'] > 0 else ''))
|
2020-07-19 15:02:53 +00:00
|
|
|
self._send_msg(message, parse_mode=ParseMode.HTML)
|
|
|
|
except RPCException as e:
|
|
|
|
self._send_msg(str(e))
|
|
|
|
|
2020-07-20 04:08:18 +00:00
|
|
|
@authorized_only
|
2020-08-04 12:49:59 +00:00
|
|
|
def _delete_trade(self, update: Update, context: CallbackContext) -> None:
|
2020-07-20 04:08:18 +00:00
|
|
|
"""
|
|
|
|
Handler for /delete <id>.
|
|
|
|
Delete the given trade
|
|
|
|
:param bot: telegram bot
|
|
|
|
:param update: message update
|
|
|
|
:return: None
|
|
|
|
"""
|
|
|
|
try:
|
2020-12-01 18:55:20 +00:00
|
|
|
if not context.args or len(context.args) == 0:
|
|
|
|
raise RPCException("Trade-id not set.")
|
|
|
|
trade_id = int(context.args[0])
|
2020-12-24 08:01:53 +00:00
|
|
|
msg = self._rpc._rpc_delete(trade_id)
|
2020-08-04 17:56:49 +00:00
|
|
|
self._send_msg((
|
2020-08-04 17:57:28 +00:00
|
|
|
'`{result_msg}`\n'
|
2020-08-04 17:56:49 +00:00
|
|
|
'Please make sure to take care of this asset on the exchange manually.'
|
|
|
|
).format(**msg))
|
2020-07-20 04:08:18 +00:00
|
|
|
|
|
|
|
except RPCException as e:
|
|
|
|
self._send_msg(str(e))
|
|
|
|
|
2018-02-13 03:45:59 +00:00
|
|
|
@authorized_only
|
2019-09-02 18:14:41 +00:00
|
|
|
def _performance(self, update: Update, context: CallbackContext) -> None:
|
2018-02-13 03:45:59 +00:00
|
|
|
"""
|
|
|
|
Handler for /performance.
|
|
|
|
Shows a performance statistic from finished trades
|
|
|
|
:param bot: telegram bot
|
|
|
|
:param update: message update
|
|
|
|
:return: None
|
|
|
|
"""
|
2018-06-08 02:52:50 +00:00
|
|
|
try:
|
2020-12-24 08:01:53 +00:00
|
|
|
trades = self._rpc._rpc_performance()
|
2021-04-03 17:12:36 +00:00
|
|
|
output = "<b>Performance:</b>\n"
|
|
|
|
for i, trade in enumerate(trades):
|
|
|
|
stat_line = (f"{i+1}.\t <code>{trade['pair']}\t{trade['profit']:.2f}% "
|
|
|
|
f"({trade['count']})</code>\n")
|
|
|
|
|
|
|
|
if len(output + stat_line) >= MAX_TELEGRAM_MESSAGE_LENGTH:
|
2021-04-13 17:20:57 +00:00
|
|
|
self._send_msg(output, parse_mode=ParseMode.HTML)
|
2021-04-03 17:12:36 +00:00
|
|
|
output = stat_line
|
|
|
|
else:
|
|
|
|
output += stat_line
|
|
|
|
|
|
|
|
self._send_msg(output, parse_mode=ParseMode.HTML)
|
2018-06-08 02:52:50 +00:00
|
|
|
except RPCException as e:
|
2019-09-02 18:14:41 +00:00
|
|
|
self._send_msg(str(e))
|
2018-02-13 03:45:59 +00:00
|
|
|
|
|
|
|
@authorized_only
|
2019-09-02 18:14:41 +00:00
|
|
|
def _count(self, update: Update, context: CallbackContext) -> None:
|
2018-02-13 03:45:59 +00:00
|
|
|
"""
|
|
|
|
Handler for /count.
|
|
|
|
Returns the number of trades running
|
|
|
|
:param bot: telegram bot
|
|
|
|
:param update: message update
|
|
|
|
:return: None
|
|
|
|
"""
|
2018-06-08 02:52:50 +00:00
|
|
|
try:
|
2020-12-24 08:01:53 +00:00
|
|
|
counts = self._rpc._rpc_count()
|
2019-04-06 17:58:45 +00:00
|
|
|
message = tabulate({k: [v] for k, v in counts.items()},
|
|
|
|
headers=['current', 'max', 'total stake'],
|
|
|
|
tablefmt='simple')
|
2018-06-08 02:52:50 +00:00
|
|
|
message = "<pre>{}</pre>".format(message)
|
|
|
|
logger.debug(message)
|
|
|
|
self._send_msg(message, parse_mode=ParseMode.HTML)
|
|
|
|
except RPCException as e:
|
2019-09-02 18:14:41 +00:00
|
|
|
self._send_msg(str(e))
|
2018-02-13 03:45:59 +00:00
|
|
|
|
2020-10-17 13:15:35 +00:00
|
|
|
@authorized_only
|
|
|
|
def _locks(self, update: Update, context: CallbackContext) -> None:
|
|
|
|
"""
|
|
|
|
Handler for /locks.
|
2020-10-20 17:39:38 +00:00
|
|
|
Returns the currently active locks
|
2020-10-17 13:15:35 +00:00
|
|
|
"""
|
2021-03-02 05:59:58 +00:00
|
|
|
locks = self._rpc._rpc_locks()
|
|
|
|
message = tabulate([[
|
|
|
|
lock['id'],
|
|
|
|
lock['pair'],
|
|
|
|
lock['lock_end_time'],
|
|
|
|
lock['reason']] for lock in locks['locks']],
|
|
|
|
headers=['ID', 'Pair', 'Until', 'Reason'],
|
|
|
|
tablefmt='simple')
|
|
|
|
message = f"<pre>{escape(message)}</pre>"
|
|
|
|
logger.debug(message)
|
|
|
|
self._send_msg(message, parse_mode=ParseMode.HTML)
|
2020-10-17 13:15:35 +00:00
|
|
|
|
2021-03-01 19:08:49 +00:00
|
|
|
@authorized_only
|
|
|
|
def _delete_locks(self, update: Update, context: CallbackContext) -> None:
|
|
|
|
"""
|
|
|
|
Handler for /delete_locks.
|
|
|
|
Returns the currently active locks
|
|
|
|
"""
|
2021-03-02 05:59:58 +00:00
|
|
|
arg = context.args[0] if context.args and len(context.args) > 0 else None
|
|
|
|
lockid = None
|
|
|
|
pair = None
|
|
|
|
if arg:
|
|
|
|
try:
|
|
|
|
lockid = int(arg)
|
|
|
|
except ValueError:
|
|
|
|
pair = arg
|
|
|
|
|
|
|
|
self._rpc._rpc_delete_lock(lockid=lockid, pair=pair)
|
|
|
|
self._locks(update, context)
|
2021-03-01 19:08:49 +00:00
|
|
|
|
2018-11-10 19:15:06 +00:00
|
|
|
@authorized_only
|
2019-09-02 18:14:41 +00:00
|
|
|
def _whitelist(self, update: Update, context: CallbackContext) -> None:
|
2018-11-10 19:15:06 +00:00
|
|
|
"""
|
|
|
|
Handler for /whitelist
|
|
|
|
Shows the currently active whitelist
|
|
|
|
"""
|
|
|
|
try:
|
2020-12-24 08:01:53 +00:00
|
|
|
whitelist = self._rpc._rpc_whitelist()
|
2018-12-03 19:31:25 +00:00
|
|
|
|
|
|
|
message = f"Using whitelist `{whitelist['method']}` with {whitelist['length']} pairs\n"
|
2018-11-10 19:15:06 +00:00
|
|
|
message += f"`{', '.join(whitelist['whitelist'])}`"
|
|
|
|
|
|
|
|
logger.debug(message)
|
|
|
|
self._send_msg(message)
|
|
|
|
except RPCException as e:
|
2019-09-02 18:14:41 +00:00
|
|
|
self._send_msg(str(e))
|
2018-11-10 19:15:06 +00:00
|
|
|
|
2019-03-24 15:08:48 +00:00
|
|
|
@authorized_only
|
2019-09-02 18:14:41 +00:00
|
|
|
def _blacklist(self, update: Update, context: CallbackContext) -> None:
|
2019-03-24 15:08:48 +00:00
|
|
|
"""
|
|
|
|
Handler for /blacklist
|
|
|
|
Shows the currently active blacklist
|
|
|
|
"""
|
|
|
|
try:
|
|
|
|
|
2020-12-24 08:01:53 +00:00
|
|
|
blacklist = self._rpc._rpc_blacklist(context.args)
|
2020-05-28 05:04:06 +00:00
|
|
|
errmsgs = []
|
|
|
|
for pair, error in blacklist['errors'].items():
|
|
|
|
errmsgs.append(f"Error adding `{pair}` to blacklist: `{error['error_msg']}`")
|
|
|
|
if errmsgs:
|
|
|
|
self._send_msg('\n'.join(errmsgs))
|
2019-03-24 15:28:14 +00:00
|
|
|
|
|
|
|
message = f"Blacklist contains {blacklist['length']} pairs\n"
|
2019-03-24 15:08:48 +00:00
|
|
|
message += f"`{', '.join(blacklist['blacklist'])}`"
|
|
|
|
|
|
|
|
logger.debug(message)
|
|
|
|
self._send_msg(message)
|
|
|
|
except RPCException as e:
|
2019-09-02 18:14:41 +00:00
|
|
|
self._send_msg(str(e))
|
2019-03-24 15:08:48 +00:00
|
|
|
|
2020-08-14 13:44:36 +00:00
|
|
|
@authorized_only
|
|
|
|
def _logs(self, update: Update, context: CallbackContext) -> None:
|
|
|
|
"""
|
|
|
|
Handler for /logs
|
|
|
|
Shows the latest logs
|
|
|
|
"""
|
|
|
|
try:
|
|
|
|
try:
|
2020-12-01 18:55:20 +00:00
|
|
|
limit = int(context.args[0]) if context.args else 10
|
2020-08-14 13:44:36 +00:00
|
|
|
except (TypeError, ValueError, IndexError):
|
|
|
|
limit = 10
|
2020-12-06 18:57:48 +00:00
|
|
|
logs = RPC._rpc_get_logs(limit)['logs']
|
2020-08-15 06:31:36 +00:00
|
|
|
msgs = ''
|
2020-08-15 18:15:02 +00:00
|
|
|
msg_template = "*{}* {}: {} \\- `{}`"
|
2020-08-14 13:44:36 +00:00
|
|
|
for logrec in logs:
|
2020-08-15 18:15:02 +00:00
|
|
|
msg = msg_template.format(escape_markdown(logrec[0], version=2),
|
|
|
|
escape_markdown(logrec[2], version=2),
|
|
|
|
escape_markdown(logrec[3], version=2),
|
|
|
|
escape_markdown(logrec[4], version=2))
|
2020-08-15 06:31:36 +00:00
|
|
|
if len(msgs + msg) + 10 >= MAX_TELEGRAM_MESSAGE_LENGTH:
|
2020-08-14 13:44:36 +00:00
|
|
|
# Send message immediately if it would become too long
|
2020-08-15 18:15:02 +00:00
|
|
|
self._send_msg(msgs, parse_mode=ParseMode.MARKDOWN_V2)
|
2020-08-15 06:31:36 +00:00
|
|
|
msgs = msg + '\n'
|
2020-08-14 13:44:36 +00:00
|
|
|
else:
|
|
|
|
# Append message to messages to send
|
2020-08-15 06:31:36 +00:00
|
|
|
msgs += msg + '\n'
|
2020-08-14 13:44:36 +00:00
|
|
|
|
2020-08-15 06:31:36 +00:00
|
|
|
if msgs:
|
2020-08-15 18:15:02 +00:00
|
|
|
self._send_msg(msgs, parse_mode=ParseMode.MARKDOWN_V2)
|
2020-08-14 13:44:36 +00:00
|
|
|
except RPCException as e:
|
|
|
|
self._send_msg(str(e))
|
|
|
|
|
2019-03-24 21:36:33 +00:00
|
|
|
@authorized_only
|
2019-09-02 18:14:41 +00:00
|
|
|
def _edge(self, update: Update, context: CallbackContext) -> None:
|
2019-03-24 21:36:33 +00:00
|
|
|
"""
|
|
|
|
Handler for /edge
|
2019-03-27 20:22:25 +00:00
|
|
|
Shows information related to Edge
|
2019-03-24 21:36:33 +00:00
|
|
|
"""
|
|
|
|
try:
|
2020-12-24 08:01:53 +00:00
|
|
|
edge_pairs = self._rpc._rpc_edge()
|
2019-03-24 21:56:42 +00:00
|
|
|
edge_pairs_tab = tabulate(edge_pairs, headers='keys', tablefmt='simple')
|
2019-03-24 21:36:33 +00:00
|
|
|
message = f'<b>Edge only validated following pairs:</b>\n<pre>{edge_pairs_tab}</pre>'
|
2019-09-02 18:14:41 +00:00
|
|
|
self._send_msg(message, parse_mode=ParseMode.HTML)
|
2019-03-24 21:36:33 +00:00
|
|
|
except RPCException as e:
|
2019-09-02 18:14:41 +00:00
|
|
|
self._send_msg(str(e))
|
2019-03-24 21:36:33 +00:00
|
|
|
|
2018-02-13 03:45:59 +00:00
|
|
|
@authorized_only
|
2019-09-02 18:14:41 +00:00
|
|
|
def _help(self, update: Update, context: CallbackContext) -> None:
|
2018-02-13 03:45:59 +00:00
|
|
|
"""
|
|
|
|
Handler for /help.
|
|
|
|
Show commands of the bot
|
|
|
|
:param bot: telegram bot
|
|
|
|
:param update: message update
|
|
|
|
:return: None
|
|
|
|
"""
|
2020-06-06 15:28:00 +00:00
|
|
|
forcebuy_text = ("*/forcebuy <pair> [<rate>]:* `Instantly buys the given pair. "
|
|
|
|
"Optionally takes a rate at which to buy.` \n")
|
|
|
|
message = ("*/start:* `Starts the trader`\n"
|
|
|
|
"*/stop:* `Stops the trader`\n"
|
2021-01-17 20:15:17 +00:00
|
|
|
"*/status <trade_id>|[table]:* `Lists all open trades`\n"
|
|
|
|
" *<trade_id> :* `Lists one or more specific trades.`\n"
|
|
|
|
" `Separate multiple <trade_id> with a blank space.`\n"
|
2020-06-06 15:28:00 +00:00
|
|
|
" *table :* `will display trades in a table`\n"
|
|
|
|
" `pending buy orders are marked with an asterisk (*)`\n"
|
|
|
|
" `pending sell orders are marked with a double asterisk (**)`\n"
|
2020-07-23 05:54:45 +00:00
|
|
|
"*/trades [limit]:* `Lists last closed trades (limited to 10 by default)`\n"
|
2020-06-06 15:28:00 +00:00
|
|
|
"*/profit:* `Lists cumulative profit from all finished trades`\n"
|
|
|
|
"*/forcesell <trade_id>|all:* `Instantly sells the given trade or all trades, "
|
|
|
|
"regardless of profit`\n"
|
|
|
|
f"{forcebuy_text if self._config.get('forcebuy_enable', False) else ''}"
|
2020-07-20 04:08:18 +00:00
|
|
|
"*/delete <trade_id>:* `Instantly delete the given trade in the database`\n"
|
2020-06-06 15:28:00 +00:00
|
|
|
"*/performance:* `Show performance of each finished trade grouped by pair`\n"
|
|
|
|
"*/daily <n>:* `Shows profit or loss per day, over the last n days`\n"
|
2020-12-07 14:03:16 +00:00
|
|
|
"*/stats:* `Shows Wins / losses by Sell reason as well as "
|
|
|
|
"Avg. holding durationsfor buys and sells.`\n"
|
2020-10-18 08:48:39 +00:00
|
|
|
"*/count:* `Show number of active trades compared to allowed number of trades`\n"
|
|
|
|
"*/locks:* `Show currently locked pairs`\n"
|
2021-03-01 19:08:49 +00:00
|
|
|
"*/unlock <pair|id>:* `Unlock this Pair (or this lock id if it's numeric)`\n"
|
2020-06-06 15:28:00 +00:00
|
|
|
"*/balance:* `Show account balance per currency`\n"
|
|
|
|
"*/stopbuy:* `Stops buying, but handles open trades gracefully` \n"
|
2020-06-09 21:03:55 +00:00
|
|
|
"*/reload_config:* `Reload configuration file` \n"
|
2020-06-06 15:28:00 +00:00
|
|
|
"*/show_config:* `Show running configuration` \n"
|
2020-08-14 13:44:36 +00:00
|
|
|
"*/logs [limit]:* `Show latest logs - defaults to 10` \n"
|
2020-06-06 15:28:00 +00:00
|
|
|
"*/whitelist:* `Show current whitelist` \n"
|
|
|
|
"*/blacklist [pair]:* `Show current blacklist, or adds one or more pairs "
|
|
|
|
"to the blacklist.` \n"
|
|
|
|
"*/edge:* `Shows validated pairs by Edge if it is enabled` \n"
|
|
|
|
"*/help:* `This help message`\n"
|
|
|
|
"*/version:* `Show version`")
|
2018-02-13 03:45:59 +00:00
|
|
|
|
2019-09-02 18:14:41 +00:00
|
|
|
self._send_msg(message)
|
2018-02-13 03:45:59 +00:00
|
|
|
|
|
|
|
@authorized_only
|
2019-09-02 18:14:41 +00:00
|
|
|
def _version(self, update: Update, context: CallbackContext) -> None:
|
2018-02-13 03:45:59 +00:00
|
|
|
"""
|
|
|
|
Handler for /version.
|
|
|
|
Show version information
|
|
|
|
:param bot: telegram bot
|
|
|
|
:param update: message update
|
|
|
|
:return: None
|
|
|
|
"""
|
2019-09-02 18:14:41 +00:00
|
|
|
self._send_msg('*Version:* `{}`'.format(__version__))
|
2018-02-13 03:45:59 +00:00
|
|
|
|
2019-11-17 14:03:45 +00:00
|
|
|
@authorized_only
|
|
|
|
def _show_config(self, update: Update, context: CallbackContext) -> None:
|
|
|
|
"""
|
|
|
|
Handler for /show_config.
|
|
|
|
Show config information information
|
|
|
|
:param bot: telegram bot
|
|
|
|
:param update: message update
|
|
|
|
:return: None
|
|
|
|
"""
|
2020-12-24 08:01:53 +00:00
|
|
|
val = RPC._rpc_show_config(self._config, self._rpc._freqtrade.state)
|
2020-11-08 10:26:02 +00:00
|
|
|
|
2019-12-13 19:27:06 +00:00
|
|
|
if val['trailing_stop']:
|
|
|
|
sl_info = (
|
|
|
|
f"*Initial Stoploss:* `{val['stoploss']}`\n"
|
|
|
|
f"*Trailing stop positive:* `{val['trailing_stop_positive']}`\n"
|
|
|
|
f"*Trailing stop offset:* `{val['trailing_stop_positive_offset']}`\n"
|
|
|
|
f"*Only trail above offset:* `{val['trailing_only_offset_is_reached']}`\n"
|
|
|
|
)
|
|
|
|
|
|
|
|
else:
|
|
|
|
sl_info = f"*Stoploss:* `{val['stoploss']}`\n"
|
|
|
|
|
2019-11-17 14:03:45 +00:00
|
|
|
self._send_msg(
|
|
|
|
f"*Mode:* `{'Dry-run' if val['dry_run'] else 'Live'}`\n"
|
|
|
|
f"*Exchange:* `{val['exchange']}`\n"
|
|
|
|
f"*Stake per trade:* `{val['stake_amount']} {val['stake_currency']}`\n"
|
2020-05-05 19:21:05 +00:00
|
|
|
f"*Max open Trades:* `{val['max_open_trades']}`\n"
|
2019-11-17 14:03:45 +00:00
|
|
|
f"*Minimum ROI:* `{val['minimal_roi']}`\n"
|
2020-06-07 08:09:39 +00:00
|
|
|
f"*Ask strategy:* ```\n{json.dumps(val['ask_strategy'])}```\n"
|
|
|
|
f"*Bid strategy:* ```\n{json.dumps(val['bid_strategy'])}```\n"
|
2019-12-13 19:27:06 +00:00
|
|
|
f"{sl_info}"
|
2020-06-01 18:43:20 +00:00
|
|
|
f"*Timeframe:* `{val['timeframe']}`\n"
|
2020-05-05 19:28:59 +00:00
|
|
|
f"*Strategy:* `{val['strategy']}`\n"
|
2020-05-05 19:21:05 +00:00
|
|
|
f"*Current state:* `{val['state']}`"
|
2019-11-17 14:03:45 +00:00
|
|
|
)
|
|
|
|
|
2020-12-01 18:55:20 +00:00
|
|
|
def _send_msg(self, msg: str, parse_mode: str = ParseMode.MARKDOWN,
|
2020-09-19 17:38:33 +00:00
|
|
|
disable_notification: bool = False) -> None:
|
2018-02-13 03:45:59 +00:00
|
|
|
"""
|
|
|
|
Send given markdown message
|
|
|
|
:param msg: message
|
|
|
|
:param bot: alternative bot
|
|
|
|
:param parse_mode: telegram parse mode
|
|
|
|
:return: None
|
|
|
|
"""
|
2021-02-04 14:27:18 +00:00
|
|
|
reply_markup = ReplyKeyboardMarkup(self._keyboard, resize_keyboard=True)
|
2017-11-17 18:47:29 +00:00
|
|
|
try:
|
2018-02-13 03:45:59 +00:00
|
|
|
try:
|
2019-09-02 18:14:41 +00:00
|
|
|
self._updater.bot.send_message(
|
2018-02-13 03:45:59 +00:00
|
|
|
self._config['telegram']['chat_id'],
|
|
|
|
text=msg,
|
|
|
|
parse_mode=parse_mode,
|
2020-09-19 17:38:33 +00:00
|
|
|
reply_markup=reply_markup,
|
|
|
|
disable_notification=disable_notification,
|
2018-02-13 03:45:59 +00:00
|
|
|
)
|
|
|
|
except NetworkError as network_err:
|
|
|
|
# Sometimes the telegram server resets the current connection,
|
|
|
|
# if this is the case we send the message again.
|
2018-03-25 19:37:14 +00:00
|
|
|
logger.warning(
|
2018-03-04 10:06:40 +00:00
|
|
|
'Telegram NetworkError: %s! Trying one more time.',
|
2018-02-13 03:45:59 +00:00
|
|
|
network_err.message
|
|
|
|
)
|
2019-09-02 18:14:41 +00:00
|
|
|
self._updater.bot.send_message(
|
2018-02-13 03:45:59 +00:00
|
|
|
self._config['telegram']['chat_id'],
|
|
|
|
text=msg,
|
|
|
|
parse_mode=parse_mode,
|
2020-09-19 17:38:33 +00:00
|
|
|
reply_markup=reply_markup,
|
|
|
|
disable_notification=disable_notification,
|
2018-02-13 03:45:59 +00:00
|
|
|
)
|
|
|
|
except TelegramError as telegram_err:
|
2018-03-25 19:37:14 +00:00
|
|
|
logger.warning(
|
2018-03-04 10:06:40 +00:00
|
|
|
'TelegramError: %s! Giving up on that message.',
|
2018-02-13 03:45:59 +00:00
|
|
|
telegram_err.message
|
2017-12-16 02:39:47 +00:00
|
|
|
)
|