stable/freqtrade/rpc/telegram.py

437 lines
13 KiB
Python
Raw Normal View History

2017-05-14 12:14:16 +00:00
import logging
2018-01-10 07:51:36 +00:00
from typing import Any, Callable
2017-05-12 17:11:56 +00:00
2017-11-20 21:26:32 +00:00
from tabulate import tabulate
2018-01-10 07:51:36 +00:00
from telegram import Bot, ParseMode, ReplyKeyboardMarkup, Update
2017-11-17 18:47:29 +00:00
from telegram.error import NetworkError, TelegramError
2017-05-12 17:11:56 +00:00
from telegram.ext import CommandHandler, Updater
from freqtrade.rpc.__init__ import (rpc_status_table,
rpc_trade_status,
rpc_daily_profit,
rpc_trade_statistics,
rpc_balance,
rpc_start,
rpc_stop,
rpc_forcesell,
rpc_performance,
rpc_count,
)
from freqtrade import __version__
2017-05-12 17:11:56 +00:00
2017-05-14 12:14:16 +00:00
# Remove noisy log messages
logging.getLogger('requests.packages.urllib3').setLevel(logging.INFO)
logging.getLogger('telegram').setLevel(logging.INFO)
2017-05-12 17:11:56 +00:00
logger = logging.getLogger(__name__)
2017-11-07 19:13:36 +00:00
_UPDATER: Updater = None
2017-09-08 21:10:22 +00:00
_CONF = {}
def init(config: dict) -> None:
"""
Initializes this module with the given config,
registers all known command handlers
and starts polling for message updates
:param config: config to use
:return: None
"""
2017-11-07 19:13:36 +00:00
global _UPDATER
2017-09-08 19:17:13 +00:00
2017-09-08 21:10:22 +00:00
_CONF.update(config)
if not is_enabled():
2017-09-28 22:15:38 +00:00
return
2017-11-07 19:13:36 +00:00
_UPDATER = Updater(token=config['telegram']['token'], workers=0)
2017-05-12 17:11:56 +00:00
# Register command handler and start telegram message polling
handles = [
CommandHandler('status', _status),
CommandHandler('profit', _profit),
CommandHandler('balance', _balance),
CommandHandler('start', _start),
CommandHandler('stop', _stop),
CommandHandler('forcesell', _forcesell),
CommandHandler('performance', _performance),
2017-12-10 06:32:40 +00:00
CommandHandler('daily', _daily),
CommandHandler('count', _count),
2017-10-21 08:08:08 +00:00
CommandHandler('help', _help),
2017-11-09 22:51:32 +00:00
CommandHandler('version', _version),
]
for handle in handles:
2017-11-07 19:13:36 +00:00
_UPDATER.dispatcher.add_handler(handle)
_UPDATER.start_polling(
clean=True,
bootstrap_retries=-1,
timeout=30,
read_latency=60,
)
2017-09-08 21:10:22 +00:00
logger.info(
'rpc.telegram is listening for following commands: %s',
[h.command for h in handles]
)
2017-05-12 17:11:56 +00:00
def cleanup() -> None:
"""
Stops all running telegram threads.
:return: None
"""
if not is_enabled():
return
2017-11-07 19:13:36 +00:00
_UPDATER.stop()
def is_enabled() -> bool:
"""
Returns True if the telegram module is activated, False otherwise
"""
return bool(_CONF['telegram'].get('enabled', False))
2017-09-01 23:22:20 +00:00
def authorized_only(command_handler: Callable[[Bot, Update], 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
"""
def wrapper(*args, **kwargs):
2017-12-16 02:39:47 +00:00
update = kwargs.get('update') or args[1]
2017-09-08 17:25:39 +00:00
2017-11-07 21:27:16 +00:00
# Reject unauthorized messages
2017-09-08 21:10:22 +00:00
chat_id = int(_CONF['telegram']['chat_id'])
2017-11-07 21:27:16 +00:00
if int(update.message.chat_id) != chat_id:
logger.info('Rejected unauthorized message from: %s', update.message.chat_id)
2017-11-07 21:27:16 +00:00
return wrapper
logger.info('Executing handler: %s for chat_id: %s', command_handler.__name__, chat_id)
try:
return command_handler(*args, **kwargs)
except BaseException:
logger.exception('Exception occurred within Telegram module')
2017-05-14 12:14:16 +00:00
return wrapper
2017-05-12 17:11:56 +00:00
@authorized_only
def _status(bot: Bot, update: Update) -> None:
"""
Handler for /status.
Returns the current TradeThread status
:param bot: telegram bot
:param update: message update
:return: None
"""
2017-10-29 22:57:48 +00:00
# Check if additional parameters are passed
params = update.message.text.replace('/status', '').split(' ') \
if update.message.text else []
if 'table' in params:
_status_table(bot, update)
return
# Fetch open trade
(error, trades) = rpc_trade_status()
if error:
send_msg(trades, bot=bot)
else:
for trademsg in trades:
send_msg(trademsg, bot=bot)
2017-10-29 22:57:48 +00:00
@authorized_only
def _status_table(bot: Bot, update: Update) -> None:
"""
Handler for /status table.
Returns the current TradeThread status in table format
:param bot: telegram bot
:param update: message update
:return: None
"""
# Fetch open trade
(err, df_statuses) = rpc_status_table()
if err:
send_msg(df_statuses, bot=bot)
2017-10-29 22:57:48 +00:00
else:
2017-11-02 17:25:19 +00:00
message = tabulate(df_statuses, headers='keys', tablefmt='simple')
2017-10-29 22:57:48 +00:00
message = "<pre>{}</pre>".format(message)
send_msg(message, parse_mode=ParseMode.HTML)
2017-12-10 06:32:40 +00:00
@authorized_only
def _daily(bot: Bot, update: Update) -> None:
"""
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
"""
try:
timescale = int(update.message.text.replace('/daily', '').strip())
except (TypeError, ValueError):
timescale = 7
(error, stats) = rpc_daily_profit(timescale,
_CONF['stake_currency'],
_CONF['fiat_display_currency'])
if error:
send_msg(stats, bot=bot)
else:
stats = tabulate(stats,
headers=[
'Day',
'Profit {}'.format(_CONF['stake_currency']),
'Profit {}'.format(_CONF['fiat_display_currency'])
],
tablefmt='simple')
message = '<b>Daily Profit over the last {} days</b>:\n<pre>{}</pre>'.format(
timescale, stats)
send_msg(message, bot=bot, parse_mode=ParseMode.HTML)
@authorized_only
def _profit(bot: Bot, update: Update) -> None:
"""
Handler for /profit.
Returns a cumulative profit statistics.
:param bot: telegram bot
:param update: message update
:return: None
"""
(error, stats) = rpc_trade_statistics(_CONF['stake_currency'],
_CONF['fiat_display_currency'])
if error:
send_msg(stats, bot=bot)
2017-09-08 17:25:39 +00:00
return
2017-12-25 07:51:41 +00:00
# Message to display
markdown_msg = """
2017-12-25 07:51:41 +00:00
*ROI:* Close trades
`{profit_closed_coin:.8f} {coin} ({profit_closed_percent:.2f}%)`
`{profit_closed_fiat:.3f} {fiat}`
*ROI:* All trades
`{profit_all_coin:.8f} {coin} ({profit_all_percent:.2f}%)`
`{profit_all_fiat:.3f} {fiat}`
*Total Trade Count:* `{trade_count}`
2017-06-08 20:52:38 +00:00
*First Trade opened:* `{first_trade_date}`
*Latest Trade opened:* `{latest_trade_date}`
*Avg. Duration:* `{avg_duration}`
2017-09-08 17:45:54 +00:00
*Best Performing:* `{best_pair}: {best_rate:.2f}%`
""".format(
2017-12-25 07:51:41 +00:00
coin=_CONF['stake_currency'],
fiat=_CONF['fiat_display_currency'],
profit_closed_coin=stats['profit_closed_coin'],
profit_closed_percent=stats['profit_closed_percent'],
profit_closed_fiat=stats['profit_closed_fiat'],
profit_all_coin=stats['profit_all_coin'],
profit_all_percent=stats['profit_all_percent'],
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']
)
send_msg(markdown_msg, bot=bot)
2017-06-08 18:01:01 +00:00
2017-06-08 20:52:38 +00:00
@authorized_only
def _balance(bot: Bot, update: Update) -> None:
"""
Handler for /balance
"""
(error, result) = rpc_balance(_CONF['fiat_display_currency'])
if error:
2018-01-28 09:15:23 +00:00
send_msg('`All balances are zero.`')
return
(currencys, total, symbol, value) = result
output = ''
for currency in currencys:
output += """*Currency*: {currency}
*Available*: {available}
*Balance*: {balance}
*Pending*: {pending}
*Est. BTC*: {est_btc: .8f}
2017-10-29 08:10:00 +00:00
""".format(**currency)
output += """*Estimated Value*:
*BTC*: {0: .8f}
*{1}*: {2: .2f}
""".format(total, symbol, value)
send_msg(output)
2017-06-08 20:52:38 +00:00
2017-10-30 23:36:35 +00:00
@authorized_only
def _start(bot: Bot, update: Update) -> None:
"""
Handler for /start.
Starts TradeThread
:param bot: telegram bot
:param update: message update
:return: None
"""
(error, msg) = rpc_start()
if error:
send_msg(msg, bot=bot)
@authorized_only
def _stop(bot: Bot, update: Update) -> None:
"""
Handler for /stop.
Stops TradeThread
:param bot: telegram bot
:param update: message update
:return: None
"""
(error, msg) = rpc_stop()
send_msg(msg, bot=bot)
# FIX: no test for this!!!!
@authorized_only
def _forcesell(bot: Bot, update: Update) -> None:
"""
Handler for /forcesell <id>.
Sells the given trade at current price
:param bot: telegram bot
:param update: message update
:return: None
"""
trade_id = update.message.text.replace('/forcesell', '').strip()
(error, message) = rpc_forcesell(trade_id)
if error:
send_msg(message, bot=bot)
return
@authorized_only
def _performance(bot: Bot, update: Update) -> None:
"""
Handler for /performance.
Shows a performance statistic from finished trades
:param bot: telegram bot
:param update: message update
:return: None
"""
(error, trades) = rpc_performance()
if error:
send_msg(trades, bot=bot)
return
2018-01-02 13:55:31 +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))
2017-11-05 14:35:15 +00:00
message = '<b>Performance:</b>\n{}'.format(stats)
send_msg(message, parse_mode=ParseMode.HTML)
@authorized_only
2017-11-05 15:26:03 +00:00
def _count(bot: Bot, update: Update) -> None:
"""
Handler for /count.
Returns the number of trades running
:param bot: telegram bot
:param update: message update
:return: None
"""
(error, trades) = rpc_count()
if error:
send_msg(trades, bot=bot)
return
2017-11-09 22:45:03 +00:00
message = tabulate({
'current': [len(trades)],
'max': [_CONF['max_open_trades']]
}, headers=['current', 'max'], tablefmt='simple')
message = "<pre>{}</pre>".format(message)
logger.debug(message)
send_msg(message, parse_mode=ParseMode.HTML)
2017-10-21 08:08:08 +00:00
@authorized_only
def _help(bot: Bot, update: Update) -> None:
"""
Handler for /help.
Show commands of the bot
:param bot: telegram bot
:param update: message update
:return: None
"""
message = """
*/start:* `Starts the trader`
*/stop:* `Stops the trader`
2017-11-02 17:25:19 +00:00
*/status [table]:* `Lists all open trades`
2017-10-29 22:57:48 +00:00
*table :* `will display trades in a table`
2017-10-21 08:08:08 +00:00
*/profit:* `Lists cumulative profit from all finished trades`
*/forcesell <trade_id>|all:* `Instantly sells the given trade or all trades, regardless of profit`
2017-10-21 08:08:08 +00:00
*/performance:* `Show performance of each finished trade grouped by pair`
*/daily <n>:* `Shows profit or loss per day, over the last n days`
2017-10-29 22:57:48 +00:00
*/count:* `Show number of trades running compared to allowed number of trades`
2017-10-29 08:10:00 +00:00
*/balance:* `Show account balance per currency`
2017-10-21 08:08:08 +00:00
*/help:* `This help message`
2017-11-09 22:51:32 +00:00
*/version:* `Show version`
2017-10-21 08:08:08 +00:00
"""
send_msg(message, bot=bot)
2017-11-09 22:51:32 +00:00
@authorized_only
def _version(bot: Bot, update: Update) -> None:
"""
Handler for /version.
Show version information
:param bot: telegram bot
:param update: message update
:return: None
"""
send_msg('*Version:* `{}`'.format(__version__), bot=bot)
2017-09-08 21:10:22 +00:00
def send_msg(msg: str, bot: Bot = None, parse_mode: ParseMode = ParseMode.MARKDOWN) -> None:
"""
Send given markdown message
:param msg: message
:param bot: alternative bot
:param parse_mode: telegram parse mode
:return: None
"""
if not is_enabled():
return
2017-11-07 21:27:16 +00:00
bot = bot or _UPDATER.bot
2017-11-17 18:47:29 +00:00
keyboard = [['/daily', '/profit', '/balance'],
['/status', '/status table', '/performance'],
['/count', '/start', '/stop', '/help']]
reply_markup = ReplyKeyboardMarkup(keyboard)
try:
2017-11-17 18:47:29 +00:00
try:
2017-12-16 02:39:47 +00:00
bot.send_message(
_CONF['telegram']['chat_id'], msg,
parse_mode=parse_mode, reply_markup=reply_markup
)
2017-11-17 18:47:29 +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.
logger.warning(
'Got Telegram NetworkError: %s! Trying one more time.',
network_err.message
)
2017-12-16 02:39:47 +00:00
bot.send_message(
_CONF['telegram']['chat_id'], msg,
parse_mode=parse_mode, reply_markup=reply_markup
)
2017-11-17 18:47:29 +00:00
except TelegramError as telegram_err:
logger.warning('Got TelegramError: %s! Giving up on that message.', telegram_err.message)