stable/freqtrade/rpc/telegram.py

973 lines
39 KiB
Python
Raw Normal View History

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
from telegram.ext import CallbackContext, CommandHandler, Updater
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__
from freqtrade.constants import DUST_PER_COIN
2020-12-22 11:34:21 +00:00
from freqtrade.exceptions import OperationalException
from freqtrade.misc import round_coin_value
from freqtrade.rpc import RPC, RPCException, RPCHandler, RPCMessageType
2020-09-28 17:39:41 +00:00
2018-03-25 19:37:14 +00:00
logger = logging.getLogger(__name__)
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
"""
2018-02-13 03:45:59 +00:00
def wrapper(self, *args, **kwargs):
""" Decorator logic """
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')
2018-02-13 03:45:59 +00:00
return wrapper
2018-03-02 15:22:00 +00:00
class Telegram(RPCHandler):
""" This class handles all telegram communication """
def __init__(self, rpc: RPC, config: Dict[str, Any]) -> None:
2018-02-13 03:45:59 +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
"""
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:
"""
Validates the keyboard configuration from telegram config
section.
"""
2020-12-20 21:51:40 +00:00
self._keyboard: List[List[Union[str, KeyboardButton]]] = [
['/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
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)
else:
self._keyboard = cust_keyboard
2020-12-22 11:34:21 +00:00
logger.info('using custom keyboard from '
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
"""
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),
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),
CommandHandler(['reload_config', 'reload_conf'], self._reload_config),
CommandHandler(['show_config', 'show_conf'], self._show_config),
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),
CommandHandler('logs', self._logs),
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(
clean=True,
bootstrap_retries=-1,
timeout=30,
read_latency=60,
)
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()
def send_msg(self, msg: Dict[str, Any]) -> None:
""" Send a message to telegram channel """
noti = self._config['telegram'].get('notification_settings', {}
2020-09-19 18:38:42 +00:00
).get(str(msg['type']), 'on')
if noti == 'off':
2020-09-19 18:04:12 +00:00
logger.info(f"Notification '{msg['type']}' not sent.")
# Notification disabled
return
if msg['type'] == RPCMessageType.BUY_NOTIFICATION:
if self._rpc._fiat_converter:
msg['stake_amount_fiat'] = self._rpc._fiat_converter.convert_amount(
2018-07-22 12:35:29 +00:00
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'])}")
2018-07-24 07:20:32 +00:00
if msg.get('fiat_currency', None):
message += f", {round_coin_value(msg['stake_amount_fiat'], msg['fiat_currency'])}"
2018-07-24 07:20:32 +00:00
message += ")`"
2020-02-08 20:02:52 +00:00
elif msg['type'] == RPCMessageType.BUY_CANCEL_NOTIFICATION:
2020-06-06 15:28:00 +00:00
message = ("\N{WARNING SIGN} *{exchange}:* "
"Cancelling open buy Order for {pair} (#{trade_id}). "
"Reason: {reason}.".format(**msg))
2020-02-08 20:02:52 +00:00
elif msg['type'] == RPCMessageType.SELL_NOTIFICATION:
msg['amount'] = round(msg['amount'], 8)
2020-02-28 09:36:39 +00:00
msg['profit_percent'] = round(msg['profit_ratio'] * 100, 2)
msg['duration'] = msg['close_date'].replace(
microsecond=0) - msg['open_date'].replace(microsecond=0)
2019-12-11 05:48:40 +00:00
msg['duration_min'] = msg['duration'].total_seconds() / 60
2020-06-06 13:38:42 +00:00
msg['emoji'] = self._get_sell_emoji(msg)
2021-03-05 22:04:12 +00:00
message = ("{emoji} *{exchange}:* Selling {pair} (#{trade_id})\n"
2018-12-04 18:58:26 +00:00
"*Amount:* `{amount:.8f}`\n"
"*Open Rate:* `{open_rate:.8f}`\n"
"*Current Rate:* `{current_rate:.8f}`\n"
2020-02-08 20:02:52 +00:00
"*Close Rate:* `{limit:.8f}`\n"
2018-12-04 18:58:26 +00:00
"*Sell Reason:* `{sell_reason}`\n"
2019-12-11 05:48:40 +00:00
"*Duration:* `{duration} ({duration_min:.1f} min)`\n"
2018-12-04 18:58:26 +00:00
"*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
2018-07-22 12:35:29 +00:00
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'])
2020-02-08 20:02:52 +00:00
message += (' `({gain}: {profit_amount:.8f} {stake_currency}'
' / {profit_fiat:.3f} {fiat_currency})`').format(**msg)
elif msg['type'] == RPCMessageType.SELL_CANCEL_NOTIFICATION:
message = ("\N{WARNING SIGN} *{exchange}:* Cancelling Open Sell Order "
"for {pair} (#{trade_id}). Reason: {reason}").format(**msg)
elif msg['type'] == RPCMessageType.STATUS_NOTIFICATION:
message = '*Status:* `{status}`'.format(**msg)
2018-08-15 02:39:32 +00:00
elif msg['type'] == RPCMessageType.WARNING_NOTIFICATION:
message = '\N{WARNING SIGN} *Warning:* `{status}`'.format(**msg)
2018-08-15 02:39:32 +00:00
elif msg['type'] == RPCMessageType.STARTUP_NOTIFICATION:
2018-08-15 02:39:32 +00:00
message = '{status}'.format(**msg)
else:
raise NotImplementedError('Unknown message type: {}'.format(msg['type']))
self._send_msg(message, disable_notification=(noti == 'silent'))
2020-06-06 13:38:42 +00:00
def _get_sell_emoji(self, msg):
"""
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
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 20:26:55 +00:00
# Check if there's at least one numerical ID provided.
# If so, try to get only these trades.
trade_ids = []
if context.args and len(context.args) > 0:
trade_ids = [int(i) for i in context.args if i.isnumeric()]
results = self._rpc._rpc_trade_status(trade_ids=trade_ids)
messages = []
for r in results:
lines = [
2019-05-06 04:55:12 +00:00
"*Trade ID:* `{trade_id}` `(since {open_date_hum})`",
"*Current Pair:* {pair}",
"*Amount:* `{amount} ({stake_amount} {base_currency})`",
"*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}%`",
]
2020-11-03 07:25:47 +00:00
if (r['stop_loss_abs'] != r['initial_stop_loss_abs']
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}` "
"`({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}` " +
("`({stop_loss_pct:.2f}%)`" if r['stop_loss_pct'] else ""))
lines.append("*Stoploss distance:* `{stoploss_current_dist:.8f}` "
"`({stoploss_current_dist_pct:.2f}%)`")
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
messages.append("\n".join([line for line in lines if line]).format(**r))
for msg in messages:
self._send_msg(msg)
2018-06-08 02:52:50 +00:00
except RPCException as e:
self._send_msg(str(e))
2018-02-13 03:45:59 +00:00
@authorized_only
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:
statlist, head = self._rpc._rpc_status_table(
self._config['stake_currency'], self._config.get('fiat_display_currency', ''))
2019-11-12 12:54:26 +00:00
message = tabulate(statlist, headers=head, tablefmt='simple')
2018-06-23 22:17:10 +00:00
self._send_msg(f"<pre>{message}</pre>", parse_mode=ParseMode.HTML)
2018-06-08 02:52:50 +00:00
except RPCException as e:
self._send_msg(str(e))
2018-02-13 03:45:59 +00:00
@authorized_only
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']
fiat_disp_cur = self._config.get('fiat_display_currency', '')
2018-02-13 03:45:59 +00:00
try:
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:
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'],
f"{round_coin_value(day['abs_profit'], stats['stake_currency'])}",
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}',
'Trades',
2020-05-17 18:12:01 +00:00
],
tablefmt='simple')
message = f'<b>Daily Profit over the last {timescale} days</b>:\n<pre>{stats_tab}</pre>'
self._send_msg(message, parse_mode=ParseMode.HTML)
2018-06-08 02:52:50 +00:00
except RPCException as e:
self._send_msg(str(e))
2018-02-13 03:45:59 +00:00
@authorized_only
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']
fiat_disp_cur = self._config.get('fiat_display_currency', '')
2018-06-23 22:17:10 +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']
if stats['trade_count'] == 0:
markdown_msg = 'No trades yet.'
2020-05-29 08:10:45 +00:00
else:
# Message to display
if stats['closed_trade_count'] > 0:
markdown_msg = ("*ROI:* Closed trades\n"
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"
f"∙ `{round_coin_value(profit_closed_fiat, fiat_disp_cur)}`\n")
else:
markdown_msg = "`No closed trade` \n"
markdown_msg += (f"*ROI:* All trades\n"
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"
f"∙ `{round_coin_value(profit_all_fiat, fiat_disp_cur)}`\n"
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']}`"
)
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
@authorized_only
def _stats(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /stats
Show stats of recent trades
"""
stats = self._rpc._rpc_stats()
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 = [
[
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()
]
sell_reasons_msg = tabulate(
sell_reasons_tabulate,
headers=['Sell Reason', 'Sells', 'Wins', 'Losses']
)
2020-12-07 13:54:39 +00:00
durations = stats['durations']
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
],
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
def _balance(self, update: Update, context: CallbackContext) -> None:
""" Handler for /balance """
2018-06-08 02:52:50 +00:00
try:
result = self._rpc._rpc_balance(self._config['stake_currency'],
self._config.get('fiat_display_currency', ''))
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)
2018-06-08 02:52:50 +00:00
output = ''
if self._config['dry_run']:
output += (
f"*Warning:* Simulated balances in Dry Mode.\n"
"This mode is still experimental!\n"
"Starting capital: "
f"`{self._config['dry_run_wallet']}` {self._config['stake_currency']}.\n"
)
for curr in result['currencies']:
if curr['est_stake'] > balance_dust_level:
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:
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:
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"
f"\t`{result['stake']}: {result['total']: .8f}`\n"
f"\t`{result['symbol']}: "
f"{round_coin_value(result['value'], result['symbol'], False)}`\n")
self._send_msg(output)
2018-06-08 02:52:50 +00:00
except RPCException as e:
self._send_msg(str(e))
2018-02-13 03:45:59 +00:00
@authorized_only
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
"""
msg = self._rpc._rpc_start()
self._send_msg('Status: `{status}`'.format(**msg))
2018-02-13 03:45:59 +00:00
@authorized_only
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
"""
msg = self._rpc._rpc_stop()
self._send_msg('Status: `{status}`'.format(**msg))
2018-02-13 03:45:59 +00:00
@authorized_only
def _reload_config(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /reload_config.
Triggers a config file reload
:param bot: telegram bot
:param update: message update
:return: None
"""
msg = self._rpc._rpc_reload_config()
self._send_msg('Status: `{status}`'.format(**msg))
@authorized_only
def _stopbuy(self, update: Update, context: CallbackContext) -> None:
"""
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
"""
msg = self._rpc._rpc_stopbuy()
self._send_msg('Status: `{status}`'.format(**msg))
2018-02-13 03:45:59 +00:00
@authorized_only
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:
msg = self._rpc._rpc_forcesell(trade_id)
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:
self._send_msg(str(e))
2018-02-13 03:45:59 +00:00
2018-10-09 17:25:43 +00:00
@authorized_only
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:
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
@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
"""
stake_cur = self._config['stake_currency']
try:
2020-12-01 18:55:20 +00:00
nrecent = int(context.args[0]) if context.args else 10
except (TypeError, ValueError, IndexError):
nrecent = 10
try:
trades = self._rpc._rpc_trade_history(
nrecent
)
trades_tab = tabulate(
[[arrow.get(trade['open_date']).humanize(),
trade['pair'],
2020-07-23 02:47:53 +00:00
f"{(100 * trade['close_profit']):.2f}% ({trade['close_profit_abs']})"]
for trade in trades['trades']],
headers=[
'Open Date',
'Pair',
f'Profit ({stake_cur})',
],
tablefmt='simple')
message = (f"<b>{min(trades['trades_count'], nrecent)} recent trades</b>:\n"
+ (f"<pre>{trades_tab}</pre>" if trades['trades_count'] > 0 else ''))
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])
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
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:
trades = self._rpc._rpc_performance()
2018-06-08 02:52:50 +00:00
stats = '\n'.join('{index}.\t<code>{pair}\t{profit:.2f}% ({count})</code>'.format(
index=i + 1,
pair=trade['pair'],
profit=trade['profit'],
count=trade['count']
) for i, trade in enumerate(trades))
message = '<b>Performance:</b>\n{}'.format(stats)
self._send_msg(message, parse_mode=ParseMode.HTML)
except RPCException as e:
self._send_msg(str(e))
2018-02-13 03:45:59 +00:00
@authorized_only
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:
counts = self._rpc._rpc_count()
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:
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
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:
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:
self._send_msg(str(e))
2018-11-10 19:15:06 +00:00
2019-03-24 15:08:48 +00:00
@authorized_only
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:
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:
self._send_msg(str(e))
2019-03-24 15:08:48 +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
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 = ''
msg_template = "*{}* {}: {} \\- `{}`"
for logrec in logs:
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:
# Send message immediately if it would become too long
self._send_msg(msgs, parse_mode=ParseMode.MARKDOWN_V2)
2020-08-15 06:31:36 +00:00
msgs = msg + '\n'
else:
# Append message to messages to send
2020-08-15 06:31:36 +00:00
msgs += msg + '\n'
2020-08-15 06:31:36 +00:00
if msgs:
self._send_msg(msgs, parse_mode=ParseMode.MARKDOWN_V2)
except RPCException as e:
self._send_msg(str(e))
2019-03-24 21:36:33 +00:00
@authorized_only
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:
edge_pairs = self._rpc._rpc_edge()
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>'
self._send_msg(message, parse_mode=ParseMode.HTML)
2019-03-24 21:36:33 +00:00
except RPCException as e:
self._send_msg(str(e))
2019-03-24 21:36:33 +00:00
2018-02-13 03:45:59 +00:00
@authorized_only
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"
"*/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"
"*/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"
"*/reload_config:* `Reload configuration file` \n"
2020-06-06 15:28:00 +00:00
"*/show_config:* `Show running configuration` \n"
"*/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
self._send_msg(message)
2018-02-13 03:45:59 +00:00
@authorized_only
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
"""
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
"""
val = RPC._rpc_show_config(self._config, self._rpc._freqtrade.state)
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"
f"{sl_info}"
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,
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
"""
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:
self._updater.bot.send_message(
2018-02-13 03:45:59 +00:00
self._config['telegram']['chat_id'],
text=msg,
parse_mode=parse_mode,
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(
'Telegram NetworkError: %s! Trying one more time.',
2018-02-13 03:45:59 +00:00
network_err.message
)
self._updater.bot.send_message(
2018-02-13 03:45:59 +00:00
self._config['telegram']['chat_id'],
text=msg,
parse_mode=parse_mode,
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(
'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
)