• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

freqtrade / freqtrade / 4131167254

pending completion
4131167254

push

github-actions

GitHub
Merge pull request #7983 from stash86/bt-metrics

16866 of 17748 relevant lines covered (95.03%)

0.95 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.53
/freqtrade/rpc/telegram.py
1
# pragma pylint: disable=unused-argument, unused-variable, protected-access, invalid-name
2

3
"""
1✔
4
This module manage Telegram communication
5
"""
6
import json
1✔
7
import logging
1✔
8
import re
1✔
9
from copy import deepcopy
1✔
10
from dataclasses import dataclass
1✔
11
from datetime import date, datetime, timedelta
1✔
12
from functools import partial
1✔
13
from html import escape
1✔
14
from itertools import chain
1✔
15
from math import isnan
1✔
16
from typing import Any, Callable, Dict, List, Optional, Union
1✔
17

18
import arrow
1✔
19
from tabulate import tabulate
1✔
20
from telegram import (MAX_MESSAGE_LENGTH, CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup,
1✔
21
                      KeyboardButton, ParseMode, ReplyKeyboardMarkup, Update)
22
from telegram.error import BadRequest, NetworkError, TelegramError
1✔
23
from telegram.ext import CallbackContext, CallbackQueryHandler, CommandHandler, Updater
1✔
24
from telegram.utils.helpers import escape_markdown
1✔
25

26
from freqtrade.__init__ import __version__
1✔
27
from freqtrade.constants import DUST_PER_COIN, Config
1✔
28
from freqtrade.enums import RPCMessageType, SignalDirection, TradingMode
1✔
29
from freqtrade.exceptions import OperationalException
1✔
30
from freqtrade.misc import chunks, plural, round_coin_value
1✔
31
from freqtrade.persistence import Trade
1✔
32
from freqtrade.rpc import RPC, RPCException, RPCHandler
1✔
33

34

35
logger = logging.getLogger(__name__)
1✔
36

37
logger.debug('Included module rpc.telegram ...')
1✔
38

39

40
@dataclass
1✔
41
class TimeunitMappings:
1✔
42
    header: str
1✔
43
    message: str
1✔
44
    message2: str
1✔
45
    callback: str
1✔
46
    default: int
1✔
47

48

49
def authorized_only(command_handler: Callable[..., None]) -> Callable[..., Any]:
1✔
50
    """
51
    Decorator to check if the message comes from the correct chat_id
52
    :param command_handler: Telegram CommandHandler
53
    :return: decorated function
54
    """
55

56
    def wrapper(self, *args, **kwargs):
1✔
57
        """ Decorator logic """
58
        update = kwargs.get('update') or args[0]
1✔
59

60
        # Reject unauthorized messages
61
        if update.callback_query:
1✔
62
            cchat_id = int(update.callback_query.message.chat.id)
1✔
63
        else:
64
            cchat_id = int(update.message.chat_id)
1✔
65

66
        chat_id = int(self._config['telegram']['chat_id'])
1✔
67
        if cchat_id != chat_id:
1✔
68
            logger.info(
1✔
69
                'Rejected unauthorized message from: %s',
70
                update.message.chat_id
71
            )
72
            return wrapper
1✔
73
        # Rollback session to avoid getting data stored in a transaction.
74
        Trade.rollback()
1✔
75
        logger.debug(
1✔
76
            'Executing handler: %s for chat_id: %s',
77
            command_handler.__name__,
78
            chat_id
79
        )
80
        try:
1✔
81
            return command_handler(self, *args, **kwargs)
1✔
82
        except RPCException as e:
1✔
83
            self._send_msg(str(e))
1✔
84
        except BaseException:
1✔
85
            logger.exception('Exception occurred within Telegram module')
1✔
86

87
    return wrapper
1✔
88

89

90
class Telegram(RPCHandler):
1✔
91
    """  This class handles all telegram communication """
92

93
    def __init__(self, rpc: RPC, config: Config) -> None:
1✔
94
        """
95
        Init the Telegram call, and init the super class RPCHandler
96
        :param rpc: instance of RPC Helper class
97
        :param config: Configuration object
98
        :return: None
99
        """
100
        super().__init__(rpc, config)
1✔
101

102
        self._updater: Updater
1✔
103
        self._init_keyboard()
1✔
104
        self._init()
1✔
105

106
    def _init_keyboard(self) -> None:
1✔
107
        """
108
        Validates the keyboard configuration from telegram config
109
        section.
110
        """
111
        self._keyboard: List[List[Union[str, KeyboardButton]]] = [
1✔
112
            ['/daily', '/profit', '/balance'],
113
            ['/status', '/status table', '/performance'],
114
            ['/count', '/start', '/stop', '/help']
115
        ]
116
        # do not allow commands with mandatory arguments and critical cmds
117
        # TODO: DRY! - its not good to list all valid cmds here. But otherwise
118
        #       this needs refactoring of the whole telegram module (same
119
        #       problem in _help()).
120
        valid_keys: List[str] = [
1✔
121
            r'/start$', r'/stop$', r'/status$', r'/status table$',
122
            r'/trades$', r'/performance$', r'/buys', r'/entries',
123
            r'/sells', r'/exits', r'/mix_tags',
124
            r'/daily$', r'/daily \d+$', r'/profit$', r'/profit \d+',
125
            r'/stats$', r'/count$', r'/locks$', r'/balance$',
126
            r'/stopbuy$', r'/stopentry$', r'/reload_config$', r'/show_config$',
127
            r'/logs$', r'/whitelist$', r'/whitelist(\ssorted|\sbaseonly)+$',
128
            r'/blacklist$', r'/bl_delete$',
129
            r'/weekly$', r'/weekly \d+$', r'/monthly$', r'/monthly \d+$',
130
            r'/forcebuy$', r'/forcelong$', r'/forceshort$',
131
            r'/forcesell$', r'/forceexit$',
132
            r'/edge$', r'/health$', r'/help$', r'/version$'
133
        ]
134
        # Create keys for generation
135
        valid_keys_print = [k.replace('$', '') for k in valid_keys]
1✔
136

137
        # custom keyboard specified in config.json
138
        cust_keyboard = self._config['telegram'].get('keyboard', [])
1✔
139
        if cust_keyboard:
1✔
140
            combined = "(" + ")|(".join(valid_keys) + ")"
1✔
141
            # check for valid shortcuts
142
            invalid_keys = [b for b in chain.from_iterable(cust_keyboard)
1✔
143
                            if not re.match(combined, b)]
144
            if len(invalid_keys):
1✔
145
                err_msg = ('config.telegram.keyboard: Invalid commands for '
1✔
146
                           f'custom Telegram keyboard: {invalid_keys}'
147
                           f'\nvalid commands are: {valid_keys_print}')
148
                raise OperationalException(err_msg)
1✔
149
            else:
150
                self._keyboard = cust_keyboard
1✔
151
                logger.info('using custom keyboard from '
1✔
152
                            f'config.json: {self._keyboard}')
153

154
    def _init(self) -> None:
1✔
155
        """
156
        Initializes this module with the given config,
157
        registers all known command handlers
158
        and starts polling for message updates
159
        """
160
        self._updater = Updater(token=self._config['telegram']['token'], workers=0,
1✔
161
                                use_context=True)
162

163
        # Register command handler and start telegram message polling
164
        handles = [
1✔
165
            CommandHandler('status', self._status),
166
            CommandHandler('profit', self._profit),
167
            CommandHandler('balance', self._balance),
168
            CommandHandler('start', self._start),
169
            CommandHandler('stop', self._stop),
170
            CommandHandler(['forcesell', 'forceexit', 'fx'], self._force_exit),
171
            CommandHandler(['forcebuy', 'forcelong'], partial(
172
                self._force_enter, order_side=SignalDirection.LONG)),
173
            CommandHandler('forceshort', partial(
174
                self._force_enter, order_side=SignalDirection.SHORT)),
175
            CommandHandler('trades', self._trades),
176
            CommandHandler('delete', self._delete_trade),
177
            CommandHandler('performance', self._performance),
178
            CommandHandler(['buys', 'entries'], self._enter_tag_performance),
179
            CommandHandler(['sells', 'exits'], self._exit_reason_performance),
180
            CommandHandler('mix_tags', self._mix_tag_performance),
181
            CommandHandler('stats', self._stats),
182
            CommandHandler('daily', self._daily),
183
            CommandHandler('weekly', self._weekly),
184
            CommandHandler('monthly', self._monthly),
185
            CommandHandler('count', self._count),
186
            CommandHandler('locks', self._locks),
187
            CommandHandler(['unlock', 'delete_locks'], self._delete_locks),
188
            CommandHandler(['reload_config', 'reload_conf'], self._reload_config),
189
            CommandHandler(['show_config', 'show_conf'], self._show_config),
190
            CommandHandler(['stopbuy', 'stopentry'], self._stopentry),
191
            CommandHandler('whitelist', self._whitelist),
192
            CommandHandler('blacklist', self._blacklist),
193
            CommandHandler(['blacklist_delete', 'bl_delete'], self._blacklist_delete),
194
            CommandHandler('logs', self._logs),
195
            CommandHandler('edge', self._edge),
196
            CommandHandler('health', self._health),
197
            CommandHandler('help', self._help),
198
            CommandHandler('version', self._version),
199
        ]
200
        callbacks = [
1✔
201
            CallbackQueryHandler(self._status_table, pattern='update_status_table'),
202
            CallbackQueryHandler(self._daily, pattern='update_daily'),
203
            CallbackQueryHandler(self._weekly, pattern='update_weekly'),
204
            CallbackQueryHandler(self._monthly, pattern='update_monthly'),
205
            CallbackQueryHandler(self._profit, pattern='update_profit'),
206
            CallbackQueryHandler(self._balance, pattern='update_balance'),
207
            CallbackQueryHandler(self._performance, pattern='update_performance'),
208
            CallbackQueryHandler(self._enter_tag_performance,
209
                                 pattern='update_enter_tag_performance'),
210
            CallbackQueryHandler(self._exit_reason_performance,
211
                                 pattern='update_exit_reason_performance'),
212
            CallbackQueryHandler(self._mix_tag_performance, pattern='update_mix_tag_performance'),
213
            CallbackQueryHandler(self._count, pattern='update_count'),
214
            CallbackQueryHandler(self._force_exit_inline, pattern=r"force_exit__\S+"),
215
            CallbackQueryHandler(self._force_enter_inline, pattern=r"\S+\/\S+"),
216
        ]
217
        for handle in handles:
1✔
218
            self._updater.dispatcher.add_handler(handle)
1✔
219

220
        for callback in callbacks:
1✔
221
            self._updater.dispatcher.add_handler(callback)
1✔
222

223
        self._updater.start_polling(
1✔
224
            bootstrap_retries=-1,
225
            timeout=20,
226
            read_latency=60,  # Assumed transmission latency
227
            drop_pending_updates=True,
228
        )
229
        logger.info(
1✔
230
            'rpc.telegram is listening for following commands: %s',
231
            [h.command for h in handles]
232
        )
233

234
    def cleanup(self) -> None:
1✔
235
        """
236
        Stops all running telegram threads.
237
        :return: None
238
        """
239
        # This can take up to `timeout` from the call to `start_polling`.
240
        self._updater.stop()
1✔
241

242
    def _exchange_from_msg(self, msg: Dict[str, Any]) -> str:
1✔
243
        """
244
        Extracts the exchange name from the given message.
245
        :param msg: The message to extract the exchange name from.
246
        :return: The exchange name.
247
        """
248
        return f"{msg['exchange']}{' (dry)' if self._config['dry_run'] else ''}"
1✔
249

250
    def _add_analyzed_candle(self, pair: str) -> str:
1✔
251
        candle_val = self._config['telegram'].get(
1✔
252
            'notification_settings', {}).get('show_candle', 'off')
253
        if candle_val != 'off':
1✔
254
            if candle_val == 'ohlc':
1✔
255
                analyzed_df, _ = self._rpc._freqtrade.dataprovider.get_analyzed_dataframe(
1✔
256
                    pair, self._config['timeframe'])
257
                candle = analyzed_df.iloc[-1].squeeze() if len(analyzed_df) > 0 else None
1✔
258
                if candle is not None:
1✔
259
                    return (
1✔
260
                        f"*Candle OHLC*: `{candle['open']}, {candle['high']}, "
261
                        f"{candle['low']}, {candle['close']}`\n"
262
                    )
263

264
        return ''
1✔
265

266
    def _format_entry_msg(self, msg: Dict[str, Any]) -> str:
1✔
267
        if self._rpc._fiat_converter:
1✔
268
            msg['stake_amount_fiat'] = self._rpc._fiat_converter.convert_amount(
1✔
269
                msg['stake_amount'], msg['stake_currency'], msg['fiat_currency'])
270
        else:
271
            msg['stake_amount_fiat'] = 0
1✔
272
        is_fill = msg['type'] in [RPCMessageType.ENTRY_FILL]
1✔
273
        emoji = '\N{CHECK MARK}' if is_fill else '\N{LARGE BLUE CIRCLE}'
1✔
274

275
        entry_side = ({'enter': 'Long', 'entered': 'Longed'} if msg['direction'] == 'Long'
1✔
276
                      else {'enter': 'Short', 'entered': 'Shorted'})
277
        message = (
1✔
278
            f"{emoji} *{self._exchange_from_msg(msg)}:*"
279
            f" {entry_side['entered'] if is_fill else entry_side['enter']} {msg['pair']}"
280
            f" (#{msg['trade_id']})\n"
281
        )
282
        message += self._add_analyzed_candle(msg['pair'])
1✔
283
        message += f"*Enter Tag:* `{msg['enter_tag']}`\n" if msg.get('enter_tag') else ""
1✔
284
        message += f"*Amount:* `{msg['amount']:.8f}`\n"
1✔
285
        if msg.get('leverage') and msg.get('leverage', 1.0) != 1.0:
1✔
286
            message += f"*Leverage:* `{msg['leverage']}`\n"
1✔
287

288
        if msg['type'] in [RPCMessageType.ENTRY_FILL]:
1✔
289
            message += f"*Open Rate:* `{msg['open_rate']:.8f}`\n"
1✔
290
        elif msg['type'] in [RPCMessageType.ENTRY]:
1✔
291
            message += f"*Open Rate:* `{msg['open_rate']:.8f}`\n"\
1✔
292
                       f"*Current Rate:* `{msg['current_rate']:.8f}`\n"
293

294
        message += f"*Total:* `({round_coin_value(msg['stake_amount'], msg['stake_currency'])}"
1✔
295

296
        if msg.get('fiat_currency'):
1✔
297
            message += f", {round_coin_value(msg['stake_amount_fiat'], msg['fiat_currency'])}"
1✔
298

299
        message += ")`"
1✔
300
        return message
1✔
301

302
    def _format_exit_msg(self, msg: Dict[str, Any]) -> str:
1✔
303
        msg['amount'] = round(msg['amount'], 8)
1✔
304
        msg['profit_percent'] = round(msg['profit_ratio'] * 100, 2)
1✔
305
        msg['duration'] = msg['close_date'].replace(
1✔
306
            microsecond=0) - msg['open_date'].replace(microsecond=0)
307
        msg['duration_min'] = msg['duration'].total_seconds() / 60
1✔
308

309
        msg['enter_tag'] = msg['enter_tag'] if "enter_tag" in msg.keys() else None
1✔
310
        msg['emoji'] = self._get_sell_emoji(msg)
1✔
311
        msg['leverage_text'] = (f"*Leverage:* `{msg['leverage']:.1f}`\n"
1✔
312
                                if msg.get('leverage') and msg.get('leverage', 1.0) != 1.0
313
                                else "")
314

315
        # Check if all sell properties are available.
316
        # This might not be the case if the message origin is triggered by /forceexit
317
        if (all(prop in msg for prop in ['gain', 'fiat_currency', 'stake_currency'])
1✔
318
                and self._rpc._fiat_converter):
319
            msg['profit_fiat'] = self._rpc._fiat_converter.convert_amount(
1✔
320
                msg['profit_amount'], msg['stake_currency'], msg['fiat_currency'])
321
            msg['profit_extra'] = (
1✔
322
                f" / {msg['profit_fiat']:.3f} {msg['fiat_currency']}")
323
        else:
324
            msg['profit_extra'] = ''
1✔
325
        msg['profit_extra'] = (
1✔
326
            f" ({msg['gain']}: {msg['profit_amount']:.8f} {msg['stake_currency']}"
327
            f"{msg['profit_extra']})")
328
        is_fill = msg['type'] == RPCMessageType.EXIT_FILL
1✔
329
        is_sub_trade = msg.get('sub_trade')
1✔
330
        is_sub_profit = msg['profit_amount'] != msg.get('cumulative_profit')
1✔
331
        profit_prefix = ('Sub ' if is_sub_profit
1✔
332
                         else 'Cumulative ') if is_sub_trade else ''
333
        cp_extra = ''
1✔
334
        if is_sub_profit and is_sub_trade:
1✔
335
            if self._rpc._fiat_converter:
1✔
336
                cp_fiat = self._rpc._fiat_converter.convert_amount(
1✔
337
                    msg['cumulative_profit'], msg['stake_currency'], msg['fiat_currency'])
338
                cp_extra = f" / {cp_fiat:.3f} {msg['fiat_currency']}"
1✔
339
            else:
340
                cp_extra = ''
×
341
            cp_extra = f"*Cumulative Profit:* (`{msg['cumulative_profit']:.8f} " \
1✔
342
                       f"{msg['stake_currency']}{cp_extra}`)\n"
343
        message = (
1✔
344
            f"{msg['emoji']} *{self._exchange_from_msg(msg)}:* "
345
            f"{'Exited' if is_fill else 'Exiting'} {msg['pair']} (#{msg['trade_id']})\n"
346
            f"{self._add_analyzed_candle(msg['pair'])}"
347
            f"*{f'{profit_prefix}Profit' if is_fill else f'Unrealized {profit_prefix}Profit'}:* "
348
            f"`{msg['profit_ratio']:.2%}{msg['profit_extra']}`\n"
349
            f"{cp_extra}"
350
            f"*Enter Tag:* `{msg['enter_tag']}`\n"
351
            f"*Exit Reason:* `{msg['exit_reason']}`\n"
352
            f"*Direction:* `{msg['direction']}`\n"
353
            f"{msg['leverage_text']}"
354
            f"*Amount:* `{msg['amount']:.8f}`\n"
355
            f"*Open Rate:* `{msg['open_rate']:.8f}`\n"
356
        )
357
        if msg['type'] == RPCMessageType.EXIT:
1✔
358
            message += f"*Current Rate:* `{msg['current_rate']:.8f}`\n"
1✔
359
            if msg['order_rate']:
1✔
360
                message += f"*Exit Rate:* `{msg['order_rate']:.8f}`"
1✔
361

362
        elif msg['type'] == RPCMessageType.EXIT_FILL:
1✔
363
            message += f"*Exit Rate:* `{msg['close_rate']:.8f}`"
1✔
364
        if msg.get('sub_trade'):
1✔
365
            if self._rpc._fiat_converter:
1✔
366
                msg['stake_amount_fiat'] = self._rpc._fiat_converter.convert_amount(
1✔
367
                    msg['stake_amount'], msg['stake_currency'], msg['fiat_currency'])
368
            else:
369
                msg['stake_amount_fiat'] = 0
×
370
            rem = round_coin_value(msg['stake_amount'], msg['stake_currency'])
1✔
371
            message += f"\n*Remaining:* `({rem}"
1✔
372

373
            if msg.get('fiat_currency', None):
1✔
374
                message += f", {round_coin_value(msg['stake_amount_fiat'], msg['fiat_currency'])}"
1✔
375

376
            message += ")`"
1✔
377
        else:
378
            message += f"\n*Duration:* `{msg['duration']} ({msg['duration_min']:.1f} min)`"
1✔
379
        return message
1✔
380

381
    def compose_message(self, msg: Dict[str, Any], msg_type: RPCMessageType) -> Optional[str]:
1✔
382
        if msg_type in [RPCMessageType.ENTRY, RPCMessageType.ENTRY_FILL]:
1✔
383
            message = self._format_entry_msg(msg)
1✔
384

385
        elif msg_type in [RPCMessageType.EXIT, RPCMessageType.EXIT_FILL]:
1✔
386
            message = self._format_exit_msg(msg)
1✔
387

388
        elif msg_type in (RPCMessageType.ENTRY_CANCEL, RPCMessageType.EXIT_CANCEL):
1✔
389
            msg['message_side'] = 'enter' if msg_type in [RPCMessageType.ENTRY_CANCEL] else 'exit'
1✔
390
            message = (f"\N{WARNING SIGN} *{self._exchange_from_msg(msg)}:* "
1✔
391
                       f"Cancelling {'partial ' if msg.get('sub_trade') else ''}"
392
                       f"{msg['message_side']} Order for {msg['pair']} "
393
                       f"(#{msg['trade_id']}). Reason: {msg['reason']}.")
394

395
        elif msg_type == RPCMessageType.PROTECTION_TRIGGER:
1✔
396
            message = (
1✔
397
                f"*Protection* triggered due to {msg['reason']}. "
398
                f"`{msg['pair']}` will be locked until `{msg['lock_end_time']}`."
399
            )
400

401
        elif msg_type == RPCMessageType.PROTECTION_TRIGGER_GLOBAL:
1✔
402
            message = (
1✔
403
                f"*Protection* triggered due to {msg['reason']}. "
404
                f"*All pairs* will be locked until `{msg['lock_end_time']}`."
405
            )
406

407
        elif msg_type == RPCMessageType.STATUS:
1✔
408
            message = f"*Status:* `{msg['status']}`"
1✔
409

410
        elif msg_type == RPCMessageType.WARNING:
1✔
411
            message = f"\N{WARNING SIGN} *Warning:* `{msg['status']}`"
1✔
412

413
        elif msg_type == RPCMessageType.STARTUP:
1✔
414
            message = f"{msg['status']}"
1✔
415
        elif msg_type == RPCMessageType.STRATEGY_MSG:
1✔
416
            message = f"{msg['msg']}"
1✔
417
        else:
418
            logger.debug("Unknown message type: %s", msg_type)
1✔
419
            return None
1✔
420
        return message
1✔
421

422
    def send_msg(self, msg: Dict[str, Any]) -> None:
1✔
423
        """ Send a message to telegram channel """
424

425
        default_noti = 'on'
1✔
426

427
        msg_type = msg['type']
1✔
428
        noti = ''
1✔
429
        if msg_type == RPCMessageType.EXIT:
1✔
430
            sell_noti = self._config['telegram'] \
1✔
431
                .get('notification_settings', {}).get(str(msg_type), {})
432
            # For backward compatibility sell still can be string
433
            if isinstance(sell_noti, str):
1✔
434
                noti = sell_noti
×
435
            else:
436
                noti = sell_noti.get(str(msg['exit_reason']), default_noti)
1✔
437
        else:
438
            noti = self._config['telegram'] \
1✔
439
                .get('notification_settings', {}).get(str(msg_type), default_noti)
440

441
        if noti == 'off':
1✔
442
            logger.info(f"Notification '{msg_type}' not sent.")
×
443
            # Notification disabled
444
            return
×
445

446
        message = self.compose_message(deepcopy(msg), msg_type)
1✔
447
        if message:
1✔
448
            self._send_msg(message, disable_notification=(noti == 'silent'))
1✔
449

450
    def _get_sell_emoji(self, msg):
1✔
451
        """
452
        Get emoji for sell-side
453
        """
454

455
        if float(msg['profit_percent']) >= 5.0:
1✔
456
            return "\N{ROCKET}"
1✔
457
        elif float(msg['profit_percent']) >= 0.0:
1✔
458
            return "\N{EIGHT SPOKED ASTERISK}"
1✔
459
        elif msg['exit_reason'] == "stop_loss":
1✔
460
            return "\N{WARNING SIGN}"
1✔
461
        else:
462
            return "\N{CROSS MARK}"
1✔
463

464
    def _prepare_order_details(self, filled_orders: List, quote_currency: str, is_open: bool):
1✔
465
        """
466
        Prepare details of trade with entry adjustment enabled
467
        """
468
        lines_detail: List[str] = []
1✔
469
        if len(filled_orders) > 0:
1✔
470
            first_avg = filled_orders[0]["safe_price"]
1✔
471

472
        for x, order in enumerate(filled_orders):
1✔
473
            lines: List[str] = []
1✔
474
            if order['is_open'] is True:
1✔
475
                continue
1✔
476
            wording = 'Entry' if order['ft_is_entry'] else 'Exit'
1✔
477

478
            cur_entry_datetime = arrow.get(order["order_filled_date"])
1✔
479
            cur_entry_amount = order["filled"] or order["amount"]
1✔
480
            cur_entry_average = order["safe_price"]
1✔
481
            lines.append("  ")
1✔
482
            if x == 0:
1✔
483
                lines.append(f"*{wording} #{x+1}:*")
1✔
484
                lines.append(
1✔
485
                    f"*Amount:* {cur_entry_amount} ({order['cost']:.8f} {quote_currency})")
486
                lines.append(f"*Average Price:* {cur_entry_average}")
1✔
487
            else:
488
                sumA = 0
1✔
489
                sumB = 0
1✔
490
                for y in range(x):
1✔
491
                    amount = filled_orders[y]["filled"] or filled_orders[y]["amount"]
1✔
492
                    sumA += amount * filled_orders[y]["safe_price"]
1✔
493
                    sumB += amount
1✔
494
                prev_avg_price = sumA / sumB
1✔
495
                # TODO: This calculation ignores fees.
496
                price_to_1st_entry = ((cur_entry_average - first_avg) / first_avg)
1✔
497
                minus_on_entry = 0
1✔
498
                if prev_avg_price:
1✔
499
                    minus_on_entry = (cur_entry_average - prev_avg_price) / prev_avg_price
1✔
500

501
                lines.append(f"*{wording} #{x+1}:* at {minus_on_entry:.2%} avg profit")
1✔
502
                if is_open:
1✔
503
                    lines.append("({})".format(cur_entry_datetime
1✔
504
                                               .humanize(granularity=["day", "hour", "minute"])))
505
                lines.append(
1✔
506
                    f"*Amount:* {cur_entry_amount} ({order['cost']:.8f} {quote_currency})")
507
                lines.append(f"*Average {wording} Price:* {cur_entry_average} "
1✔
508
                             f"({price_to_1st_entry:.2%} from 1st entry rate)")
509
                lines.append(f"*Order filled:* {order['order_filled_date']}")
1✔
510

511
                # TODO: is this really useful?
512
                # dur_entry = cur_entry_datetime - arrow.get(
513
                #     filled_orders[x - 1]["order_filled_date"])
514
                # days = dur_entry.days
515
                # hours, remainder = divmod(dur_entry.seconds, 3600)
516
                # minutes, seconds = divmod(remainder, 60)
517
                # lines.append(
518
                # f"({days}d {hours}h {minutes}m {seconds}s from previous {wording.lower()})")
519
            lines_detail.append("\n".join(lines))
1✔
520
        return lines_detail
1✔
521

522
    @authorized_only
1✔
523
    def _status(self, update: Update, context: CallbackContext) -> None:
1✔
524
        """
525
        Handler for /status.
526
        Returns the current TradeThread status
527
        :param bot: telegram bot
528
        :param update: message update
529
        :return: None
530
        """
531

532
        if context.args and 'table' in context.args:
1✔
533
            self._status_table(update, context)
1✔
534
            return
1✔
535
        else:
536
            self._status_msg(update, context)
1✔
537

538
    def _status_msg(self, update: Update, context: CallbackContext) -> None:
1✔
539
        """
540
        handler for `/status` and `/status <id>`.
541

542
        """
543
        # Check if there's at least one numerical ID provided.
544
        # If so, try to get only these trades.
545
        trade_ids = []
1✔
546
        if context.args and len(context.args) > 0:
1✔
547
            trade_ids = [int(i) for i in context.args if i.isnumeric()]
1✔
548

549
        results = self._rpc._rpc_trade_status(trade_ids=trade_ids)
1✔
550
        position_adjust = self._config.get('position_adjustment_enable', False)
1✔
551
        max_entries = self._config.get('max_entry_position_adjustment', -1)
1✔
552
        for r in results:
1✔
553
            r['open_date_hum'] = arrow.get(r['open_date']).humanize()
1✔
554
            r['num_entries'] = len([o for o in r['orders'] if o['ft_is_entry']])
1✔
555
            r['exit_reason'] = r.get('exit_reason', "")
1✔
556
            lines = [
1✔
557
                "*Trade ID:* `{trade_id}`" +
558
                (" `(since {open_date_hum})`" if r['is_open'] else ""),
559
                "*Current Pair:* {pair}",
560
                "*Direction:* " + ("`Short`" if r.get('is_short') else "`Long`"),
561
                "*Leverage:* `{leverage}`" if r.get('leverage') else "",
562
                "*Amount:* `{amount} ({stake_amount} {quote_currency})`",
563
                "*Enter Tag:* `{enter_tag}`" if r['enter_tag'] else "",
564
                "*Exit Reason:* `{exit_reason}`" if r['exit_reason'] else "",
565
            ]
566

567
            if position_adjust:
1✔
568
                max_buy_str = (f"/{max_entries + 1}" if (max_entries > 0) else "")
1✔
569
                lines.append("*Number of Entries:* `{num_entries}`" + max_buy_str)
1✔
570

571
            lines.extend([
1✔
572
                "*Open Rate:* `{open_rate:.8f}`",
573
                "*Close Rate:* `{close_rate:.8f}`" if r['close_rate'] else "",
574
                "*Open Date:* `{open_date}`",
575
                "*Close Date:* `{close_date}`" if r['close_date'] else "",
576
                "*Current Rate:* `{current_rate:.8f}`" if r['is_open'] else "",
577
                ("*Current Profit:* " if r['is_open'] else "*Close Profit: *")
578
                + "`{profit_ratio:.2%}`",
579
            ])
580

581
            if r['is_open']:
1✔
582
                if r.get('realized_profit'):
1✔
583
                    lines.append("*Realized Profit:* `{realized_profit:.8f}`")
×
584
                if (r['stop_loss_abs'] != r['initial_stop_loss_abs']
1✔
585
                        and r['initial_stop_loss_ratio'] is not None):
586
                    # Adding initial stoploss only if it is different from stoploss
587
                    lines.append("*Initial Stoploss:* `{initial_stop_loss_abs:.8f}` "
1✔
588
                                 "`({initial_stop_loss_ratio:.2%})`")
589

590
                # Adding stoploss and stoploss percentage only if it is not None
591
                lines.append("*Stoploss:* `{stop_loss_abs:.8f}` " +
1✔
592
                             ("`({stop_loss_ratio:.2%})`" if r['stop_loss_ratio'] else ""))
593
                lines.append("*Stoploss distance:* `{stoploss_current_dist:.8f}` "
1✔
594
                             "`({stoploss_current_dist_ratio:.2%})`")
595
                if r['open_order']:
1✔
596
                    lines.append(
1✔
597
                        "*Open Order:* `{open_order}`"
598
                        + "- `{exit_order_status}`" if r['exit_order_status'] else "")
599

600
            lines_detail = self._prepare_order_details(
1✔
601
                r['orders'], r['quote_currency'], r['is_open'])
602
            lines.extend(lines_detail if lines_detail else "")
1✔
603
            self.__send_status_msg(lines, r)
1✔
604

605
    def __send_status_msg(self, lines: List[str], r: Dict[str, Any]) -> None:
1✔
606
        """
607
        Send status message.
608
        """
609
        msg = ''
1✔
610

611
        for line in lines:
1✔
612
            if line:
1✔
613
                if (len(msg) + len(line) + 1) < MAX_MESSAGE_LENGTH:
1✔
614
                    msg += line + '\n'
1✔
615
                else:
616
                    self._send_msg(msg.format(**r))
1✔
617
                    msg = "*Trade ID:* `{trade_id}` - continued\n" + line + '\n'
1✔
618

619
        self._send_msg(msg.format(**r))
1✔
620

621
    @authorized_only
1✔
622
    def _status_table(self, update: Update, context: CallbackContext) -> None:
1✔
623
        """
624
        Handler for /status table.
625
        Returns the current TradeThread status in table format
626
        :param bot: telegram bot
627
        :param update: message update
628
        :return: None
629
        """
630
        fiat_currency = self._config.get('fiat_display_currency', '')
1✔
631
        statlist, head, fiat_profit_sum = self._rpc._rpc_status_table(
1✔
632
            self._config['stake_currency'], fiat_currency)
633

634
        show_total = not isnan(fiat_profit_sum) and len(statlist) > 1
1✔
635
        max_trades_per_msg = 50
1✔
636
        """
1✔
637
        Calculate the number of messages of 50 trades per message
638
        0.99 is used to make sure that there are no extra (empty) messages
639
        As an example with 50 trades, there will be int(50/50 + 0.99) = 1 message
640
        """
641
        messages_count = max(int(len(statlist) / max_trades_per_msg + 0.99), 1)
1✔
642
        for i in range(0, messages_count):
1✔
643
            trades = statlist[i * max_trades_per_msg:(i + 1) * max_trades_per_msg]
1✔
644
            if show_total and i == messages_count - 1:
1✔
645
                # append total line
646
                trades.append(["Total", "", "", f"{fiat_profit_sum:.2f} {fiat_currency}"])
×
647

648
            message = tabulate(trades,
1✔
649
                               headers=head,
650
                               tablefmt='simple')
651
            if show_total and i == messages_count - 1:
1✔
652
                # insert separators line between Total
653
                lines = message.split("\n")
×
654
                message = "\n".join(lines[:-1] + [lines[1]] + [lines[-1]])
×
655
            self._send_msg(f"<pre>{message}</pre>", parse_mode=ParseMode.HTML,
1✔
656
                           reload_able=True, callback_path="update_status_table",
657
                           query=update.callback_query)
658

659
    @authorized_only
1✔
660
    def _timeunit_stats(self, update: Update, context: CallbackContext, unit: str) -> None:
1✔
661
        """
662
        Handler for /daily <n>
663
        Returns a daily profit (in BTC) over the last n days.
664
        :param bot: telegram bot
665
        :param update: message update
666
        :return: None
667
        """
668

669
        vals = {
1✔
670
            'days': TimeunitMappings('Day', 'Daily', 'days', 'update_daily', 7),
671
            'weeks': TimeunitMappings('Monday', 'Weekly', 'weeks (starting from Monday)',
672
                                      'update_weekly', 8),
673
            'months': TimeunitMappings('Month', 'Monthly', 'months', 'update_monthly', 6),
674
        }
675
        val = vals[unit]
1✔
676

677
        stake_cur = self._config['stake_currency']
1✔
678
        fiat_disp_cur = self._config.get('fiat_display_currency', '')
1✔
679
        try:
1✔
680
            timescale = int(context.args[0]) if context.args else val.default
1✔
681
        except (TypeError, ValueError, IndexError):
1✔
682
            timescale = val.default
1✔
683
        stats = self._rpc._rpc_timeunit_profit(
1✔
684
            timescale,
685
            stake_cur,
686
            fiat_disp_cur,
687
            unit
688
        )
689
        stats_tab = tabulate(
1✔
690
            [[f"{period['date']} ({period['trade_count']})",
691
              f"{round_coin_value(period['abs_profit'], stats['stake_currency'])}",
692
              f"{period['fiat_value']:.2f} {stats['fiat_display_currency']}",
693
              f"{period['rel_profit']:.2%}",
694
              ] for period in stats['data']],
695
            headers=[
696
                f"{val.header} (count)",
697
                f'{stake_cur}',
698
                f'{fiat_disp_cur}',
699
                'Profit %',
700
                'Trades',
701
            ],
702
            tablefmt='simple')
703
        message = (
1✔
704
            f'<b>{val.message} Profit over the last {timescale} {val.message2}</b>:\n'
705
            f'<pre>{stats_tab}</pre>'
706
        )
707
        self._send_msg(message, parse_mode=ParseMode.HTML, reload_able=True,
1✔
708
                       callback_path=val.callback, query=update.callback_query)
709

710
    @authorized_only
1✔
711
    def _daily(self, update: Update, context: CallbackContext) -> None:
1✔
712
        """
713
        Handler for /daily <n>
714
        Returns a daily profit (in BTC) over the last n days.
715
        :param bot: telegram bot
716
        :param update: message update
717
        :return: None
718
        """
719
        self._timeunit_stats(update, context, 'days')
1✔
720

721
    @authorized_only
1✔
722
    def _weekly(self, update: Update, context: CallbackContext) -> None:
1✔
723
        """
724
        Handler for /weekly <n>
725
        Returns a weekly profit (in BTC) over the last n weeks.
726
        :param bot: telegram bot
727
        :param update: message update
728
        :return: None
729
        """
730
        self._timeunit_stats(update, context, 'weeks')
1✔
731

732
    @authorized_only
1✔
733
    def _monthly(self, update: Update, context: CallbackContext) -> None:
1✔
734
        """
735
        Handler for /monthly <n>
736
        Returns a monthly profit (in BTC) over the last n months.
737
        :param bot: telegram bot
738
        :param update: message update
739
        :return: None
740
        """
741
        self._timeunit_stats(update, context, 'months')
1✔
742

743
    @authorized_only
1✔
744
    def _profit(self, update: Update, context: CallbackContext) -> None:
1✔
745
        """
746
        Handler for /profit.
747
        Returns a cumulative profit statistics.
748
        :param bot: telegram bot
749
        :param update: message update
750
        :return: None
751
        """
752
        stake_cur = self._config['stake_currency']
1✔
753
        fiat_disp_cur = self._config.get('fiat_display_currency', '')
1✔
754

755
        start_date = datetime.fromtimestamp(0)
1✔
756
        timescale = None
1✔
757
        try:
1✔
758
            if context.args:
1✔
759
                timescale = int(context.args[0]) - 1
1✔
760
                today_start = datetime.combine(date.today(), datetime.min.time())
1✔
761
                start_date = today_start - timedelta(days=timescale)
1✔
762
        except (TypeError, ValueError, IndexError):
1✔
763
            pass
1✔
764

765
        stats = self._rpc._rpc_trade_statistics(
1✔
766
            stake_cur,
767
            fiat_disp_cur,
768
            start_date)
769
        profit_closed_coin = stats['profit_closed_coin']
1✔
770
        profit_closed_ratio_mean = stats['profit_closed_ratio_mean']
1✔
771
        profit_closed_percent = stats['profit_closed_percent']
1✔
772
        profit_closed_fiat = stats['profit_closed_fiat']
1✔
773
        profit_all_coin = stats['profit_all_coin']
1✔
774
        profit_all_ratio_mean = stats['profit_all_ratio_mean']
1✔
775
        profit_all_percent = stats['profit_all_percent']
1✔
776
        profit_all_fiat = stats['profit_all_fiat']
1✔
777
        trade_count = stats['trade_count']
1✔
778
        first_trade_date = stats['first_trade_date']
1✔
779
        latest_trade_date = stats['latest_trade_date']
1✔
780
        avg_duration = stats['avg_duration']
1✔
781
        best_pair = stats['best_pair']
1✔
782
        best_pair_profit_ratio = stats['best_pair_profit_ratio']
1✔
783
        if stats['trade_count'] == 0:
1✔
784
            markdown_msg = 'No trades yet.'
1✔
785
        else:
786
            # Message to display
787
            if stats['closed_trade_count'] > 0:
1✔
788
                markdown_msg = ("*ROI:* Closed trades\n"
1✔
789
                                f"∙ `{round_coin_value(profit_closed_coin, stake_cur)} "
790
                                f"({profit_closed_ratio_mean:.2%}) "
791
                                f"({profit_closed_percent} \N{GREEK CAPITAL LETTER SIGMA}%)`\n"
792
                                f"∙ `{round_coin_value(profit_closed_fiat, fiat_disp_cur)}`\n")
793
            else:
794
                markdown_msg = "`No closed trade` \n"
1✔
795

796
            markdown_msg += (
1✔
797
                f"*ROI:* All trades\n"
798
                f"∙ `{round_coin_value(profit_all_coin, stake_cur)} "
799
                f"({profit_all_ratio_mean:.2%}) "
800
                f"({profit_all_percent} \N{GREEK CAPITAL LETTER SIGMA}%)`\n"
801
                f"∙ `{round_coin_value(profit_all_fiat, fiat_disp_cur)}`\n"
802
                f"*Total Trade Count:* `{trade_count}`\n"
803
                f"*{'First Trade opened' if not timescale else 'Showing Profit since'}:* "
804
                f"`{first_trade_date}`\n"
805
                f"*Latest Trade opened:* `{latest_trade_date}`\n"
806
                f"*Win / Loss:* `{stats['winning_trades']} / {stats['losing_trades']}`"
807
            )
808
            if stats['closed_trade_count'] > 0:
1✔
809
                markdown_msg += (
1✔
810
                    f"\n*Avg. Duration:* `{avg_duration}`\n"
811
                    f"*Best Performing:* `{best_pair}: {best_pair_profit_ratio:.2%}`\n"
812
                    f"*Trading volume:* `{round_coin_value(stats['trading_volume'], stake_cur)}`\n"
813
                    f"*Profit factor:* `{stats['profit_factor']:.2f}`\n"
814
                    f"*Max Drawdown:* `{stats['max_drawdown']:.2%} "
815
                    f"({round_coin_value(stats['max_drawdown_abs'], stake_cur)})`"
816
                )
817
        self._send_msg(markdown_msg, reload_able=True, callback_path="update_profit",
1✔
818
                       query=update.callback_query)
819

820
    @authorized_only
1✔
821
    def _stats(self, update: Update, context: CallbackContext) -> None:
1✔
822
        """
823
        Handler for /stats
824
        Show stats of recent trades
825
        """
826
        stats = self._rpc._rpc_stats()
1✔
827

828
        reason_map = {
1✔
829
            'roi': 'ROI',
830
            'stop_loss': 'Stoploss',
831
            'trailing_stop_loss': 'Trail. Stop',
832
            'stoploss_on_exchange': 'Stoploss',
833
            'exit_signal': 'Exit Signal',
834
            'force_exit': 'Force Exit',
835
            'emergency_exit': 'Emergency Exit',
836
        }
837
        exit_reasons_tabulate = [
1✔
838
            [
839
                reason_map.get(reason, reason),
840
                sum(count.values()),
841
                count['wins'],
842
                count['losses']
843
            ] for reason, count in stats['exit_reasons'].items()
844
        ]
845
        exit_reasons_msg = 'No trades yet.'
1✔
846
        for reason in chunks(exit_reasons_tabulate, 25):
1✔
847
            exit_reasons_msg = tabulate(
1✔
848
                reason,
849
                headers=['Exit Reason', 'Exits', 'Wins', 'Losses']
850
            )
851
            if len(exit_reasons_tabulate) > 25:
1✔
852
                self._send_msg(f"```\n{exit_reasons_msg}```", ParseMode.MARKDOWN)
×
853
                exit_reasons_msg = ''
×
854

855
        durations = stats['durations']
1✔
856
        duration_msg = tabulate(
1✔
857
            [
858
                ['Wins', str(timedelta(seconds=durations['wins']))
859
                 if durations['wins'] is not None else 'N/A'],
860
                ['Losses', str(timedelta(seconds=durations['losses']))
861
                 if durations['losses'] is not None else 'N/A']
862
            ],
863
            headers=['', 'Avg. Duration']
864
        )
865
        msg = (f"""```\n{exit_reasons_msg}```\n```\n{duration_msg}```""")
1✔
866

867
        self._send_msg(msg, ParseMode.MARKDOWN)
1✔
868

869
    @authorized_only
1✔
870
    def _balance(self, update: Update, context: CallbackContext) -> None:
1✔
871
        """ Handler for /balance """
872
        result = self._rpc._rpc_balance(self._config['stake_currency'],
1✔
873
                                        self._config.get('fiat_display_currency', ''))
874

875
        balance_dust_level = self._config['telegram'].get('balance_dust_level', 0.0)
1✔
876
        if not balance_dust_level:
1✔
877
            balance_dust_level = DUST_PER_COIN.get(self._config['stake_currency'], 1.0)
1✔
878

879
        output = ''
1✔
880
        if self._config['dry_run']:
1✔
881
            output += "*Warning:* Simulated balances in Dry Mode.\n"
1✔
882
        starting_cap = round_coin_value(
1✔
883
            result['starting_capital'], self._config['stake_currency'])
884
        output += f"Starting capital: `{starting_cap}`"
1✔
885
        starting_cap_fiat = round_coin_value(
1✔
886
            result['starting_capital_fiat'], self._config['fiat_display_currency']
887
        ) if result['starting_capital_fiat'] > 0 else ''
888
        output += (f" `, {starting_cap_fiat}`.\n"
1✔
889
                   ) if result['starting_capital_fiat'] > 0 else '.\n'
890

891
        total_dust_balance = 0
1✔
892
        total_dust_currencies = 0
1✔
893
        for curr in result['currencies']:
1✔
894
            curr_output = ''
1✔
895
            if curr['est_stake'] > balance_dust_level:
1✔
896
                if curr['is_position']:
1✔
897
                    curr_output = (
×
898
                        f"*{curr['currency']}:*\n"
899
                        f"\t`{curr['side']}: {curr['position']:.8f}`\n"
900
                        f"\t`Leverage: {curr['leverage']:.1f}`\n"
901
                        f"\t`Est. {curr['stake']}: "
902
                        f"{round_coin_value(curr['est_stake'], curr['stake'], False)}`\n")
903
                else:
904
                    curr_output = (
1✔
905
                        f"*{curr['currency']}:*\n"
906
                        f"\t`Available: {curr['free']:.8f}`\n"
907
                        f"\t`Balance: {curr['balance']:.8f}`\n"
908
                        f"\t`Pending: {curr['used']:.8f}`\n"
909
                        f"\t`Est. {curr['stake']}: "
910
                        f"{round_coin_value(curr['est_stake'], curr['stake'], False)}`\n")
911
            elif curr['est_stake'] <= balance_dust_level:
1✔
912
                total_dust_balance += curr['est_stake']
1✔
913
                total_dust_currencies += 1
1✔
914

915
            # Handle overflowing message length
916
            if len(output + curr_output) >= MAX_MESSAGE_LENGTH:
1✔
917
                self._send_msg(output)
1✔
918
                output = curr_output
1✔
919
            else:
920
                output += curr_output
1✔
921

922
        if total_dust_balance > 0:
1✔
923
            output += (
1✔
924
                f"*{total_dust_currencies} Other "
925
                f"{plural(total_dust_currencies, 'Currency', 'Currencies')} "
926
                f"(< {balance_dust_level} {result['stake']}):*\n"
927
                f"\t`Est. {result['stake']}: "
928
                f"{round_coin_value(total_dust_balance, result['stake'], False)}`\n")
929
        tc = result['trade_count'] > 0
1✔
930
        stake_improve = f" `({result['starting_capital_ratio']:.2%})`" if tc else ''
1✔
931
        fiat_val = f" `({result['starting_capital_fiat_ratio']:.2%})`" if tc else ''
1✔
932

933
        output += ("\n*Estimated Value*:\n"
1✔
934
                   f"\t`{result['stake']}: "
935
                   f"{round_coin_value(result['total'], result['stake'], False)}`"
936
                   f"{stake_improve}\n"
937
                   f"\t`{result['symbol']}: "
938
                   f"{round_coin_value(result['value'], result['symbol'], False)}`"
939
                   f"{fiat_val}\n")
940
        self._send_msg(output, reload_able=True, callback_path="update_balance",
1✔
941
                       query=update.callback_query)
942

943
    @authorized_only
1✔
944
    def _start(self, update: Update, context: CallbackContext) -> None:
1✔
945
        """
946
        Handler for /start.
947
        Starts TradeThread
948
        :param bot: telegram bot
949
        :param update: message update
950
        :return: None
951
        """
952
        msg = self._rpc._rpc_start()
1✔
953
        self._send_msg(f"Status: `{msg['status']}`")
1✔
954

955
    @authorized_only
1✔
956
    def _stop(self, update: Update, context: CallbackContext) -> None:
1✔
957
        """
958
        Handler for /stop.
959
        Stops TradeThread
960
        :param bot: telegram bot
961
        :param update: message update
962
        :return: None
963
        """
964
        msg = self._rpc._rpc_stop()
1✔
965
        self._send_msg(f"Status: `{msg['status']}`")
1✔
966

967
    @authorized_only
1✔
968
    def _reload_config(self, update: Update, context: CallbackContext) -> None:
1✔
969
        """
970
        Handler for /reload_config.
971
        Triggers a config file reload
972
        :param bot: telegram bot
973
        :param update: message update
974
        :return: None
975
        """
976
        msg = self._rpc._rpc_reload_config()
1✔
977
        self._send_msg(f"Status: `{msg['status']}`")
1✔
978

979
    @authorized_only
1✔
980
    def _stopentry(self, update: Update, context: CallbackContext) -> None:
1✔
981
        """
982
        Handler for /stop_buy.
983
        Sets max_open_trades to 0 and gracefully sells all open trades
984
        :param bot: telegram bot
985
        :param update: message update
986
        :return: None
987
        """
988
        msg = self._rpc._rpc_stopentry()
1✔
989
        self._send_msg(f"Status: `{msg['status']}`")
1✔
990

991
    @authorized_only
1✔
992
    def _force_exit(self, update: Update, context: CallbackContext) -> None:
1✔
993
        """
994
        Handler for /forceexit <id>.
995
        Sells the given trade at current price
996
        :param bot: telegram bot
997
        :param update: message update
998
        :return: None
999
        """
1000

1001
        if context.args:
1✔
1002
            trade_id = context.args[0]
1✔
1003
            self._force_exit_action(trade_id)
1✔
1004
        else:
1005
            fiat_currency = self._config.get('fiat_display_currency', '')
1✔
1006
            try:
1✔
1007
                statlist, _, _ = self._rpc._rpc_status_table(
1✔
1008
                    self._config['stake_currency'], fiat_currency)
1009
            except RPCException:
1✔
1010
                self._send_msg(msg='No open trade found.')
1✔
1011
                return
1✔
1012
            trades = []
1✔
1013
            for trade in statlist:
1✔
1014
                trades.append((trade[0], f"{trade[0]} {trade[1]} {trade[2]} {trade[3]}"))
1✔
1015

1016
            trade_buttons = [
1✔
1017
                InlineKeyboardButton(text=trade[1], callback_data=f"force_exit__{trade[0]}")
1018
                for trade in trades]
1019
            buttons_aligned = self._layout_inline_keyboard_onecol(trade_buttons)
1✔
1020

1021
            buttons_aligned.append([InlineKeyboardButton(
1✔
1022
                text='Cancel', callback_data='force_exit__cancel')])
1023
            self._send_msg(msg="Which trade?", keyboard=buttons_aligned)
1✔
1024

1025
    def _force_exit_action(self, trade_id):
1✔
1026
        if trade_id != 'cancel':
1✔
1027
            try:
1✔
1028
                self._rpc._rpc_force_exit(trade_id)
1✔
1029
            except RPCException as e:
1✔
1030
                self._send_msg(str(e))
1✔
1031

1032
    def _force_exit_inline(self, update: Update, _: CallbackContext) -> None:
1✔
1033
        if update.callback_query:
1✔
1034
            query = update.callback_query
1✔
1035
            if query.data and '__' in query.data:
1✔
1036
                # Input data is "force_exit__<tradid|cancel>"
1037
                trade_id = query.data.split("__")[1].split(' ')[0]
1✔
1038
                if trade_id == 'cancel':
1✔
1039
                    query.answer()
1✔
1040
                    query.edit_message_text(text="Force exit canceled.")
1✔
1041
                    return
1✔
1042
                trade: Trade = Trade.get_trades(trade_filter=Trade.id == trade_id).first()
1✔
1043
                query.answer()
1✔
1044
                query.edit_message_text(text=f"Manually exiting Trade #{trade_id}, {trade.pair}")
1✔
1045
                self._force_exit_action(trade_id)
1✔
1046

1047
    def _force_enter_action(self, pair, price: Optional[float], order_side: SignalDirection):
1✔
1048
        if pair != 'cancel':
1✔
1049
            try:
1✔
1050
                self._rpc._rpc_force_entry(pair, price, order_side=order_side)
1✔
1051
            except RPCException as e:
1✔
1052
                logger.exception("Forcebuy error!")
1✔
1053
                self._send_msg(str(e), ParseMode.HTML)
1✔
1054

1055
    def _force_enter_inline(self, update: Update, _: CallbackContext) -> None:
1✔
1056
        if update.callback_query:
1✔
1057
            query = update.callback_query
1✔
1058
            if query.data and '_||_' in query.data:
1✔
1059
                pair, side = query.data.split('_||_')
1✔
1060
                order_side = SignalDirection(side)
1✔
1061
                query.answer()
1✔
1062
                query.edit_message_text(text=f"Manually entering {order_side} for {pair}")
1✔
1063
                self._force_enter_action(pair, None, order_side)
1✔
1064

1065
    @staticmethod
1✔
1066
    def _layout_inline_keyboard(
1✔
1067
            buttons: List[InlineKeyboardButton], cols=3) -> List[List[InlineKeyboardButton]]:
1068
        return [buttons[i:i + cols] for i in range(0, len(buttons), cols)]
1✔
1069

1070
    @staticmethod
1✔
1071
    def _layout_inline_keyboard_onecol(
1✔
1072
            buttons: List[InlineKeyboardButton], cols=1) -> List[List[InlineKeyboardButton]]:
1073
        return [buttons[i:i + cols] for i in range(0, len(buttons), cols)]
1✔
1074

1075
    @authorized_only
1✔
1076
    def _force_enter(
1✔
1077
            self, update: Update, context: CallbackContext, order_side: SignalDirection) -> None:
1078
        """
1079
        Handler for /forcelong <asset> <price> and `/forceshort <asset> <price>
1080
        Buys a pair trade at the given or current price
1081
        :param bot: telegram bot
1082
        :param update: message update
1083
        :return: None
1084
        """
1085
        if context.args:
1✔
1086
            pair = context.args[0]
1✔
1087
            price = float(context.args[1]) if len(context.args) > 1 else None
1✔
1088
            self._force_enter_action(pair, price, order_side)
1✔
1089
        else:
1090
            whitelist = self._rpc._rpc_whitelist()['whitelist']
1✔
1091
            pair_buttons = [
1✔
1092
                InlineKeyboardButton(text=pair, callback_data=f"{pair}_||_{order_side}")
1093
                for pair in sorted(whitelist)
1094
            ]
1095
            buttons_aligned = self._layout_inline_keyboard(pair_buttons)
1✔
1096

1097
            buttons_aligned.append([InlineKeyboardButton(text='Cancel', callback_data='cancel')])
1✔
1098
            self._send_msg(msg="Which pair?",
1✔
1099
                           keyboard=buttons_aligned,
1100
                           query=update.callback_query)
1101

1102
    @authorized_only
1✔
1103
    def _trades(self, update: Update, context: CallbackContext) -> None:
1✔
1104
        """
1105
        Handler for /trades <n>
1106
        Returns last n recent trades.
1107
        :param bot: telegram bot
1108
        :param update: message update
1109
        :return: None
1110
        """
1111
        stake_cur = self._config['stake_currency']
1✔
1112
        try:
1✔
1113
            nrecent = int(context.args[0]) if context.args else 10
1✔
1114
        except (TypeError, ValueError, IndexError):
1✔
1115
            nrecent = 10
1✔
1116
        trades = self._rpc._rpc_trade_history(
1✔
1117
            nrecent
1118
        )
1119
        trades_tab = tabulate(
1✔
1120
            [[arrow.get(trade['close_date']).humanize(),
1121
                trade['pair'] + " (#" + str(trade['trade_id']) + ")",
1122
                f"{(trade['close_profit']):.2%} ({trade['close_profit_abs']})"]
1123
                for trade in trades['trades']],
1124
            headers=[
1125
                'Close Date',
1126
                'Pair (ID)',
1127
                f'Profit ({stake_cur})',
1128
            ],
1129
            tablefmt='simple')
1130
        message = (f"<b>{min(trades['trades_count'], nrecent)} recent trades</b>:\n"
1✔
1131
                   + (f"<pre>{trades_tab}</pre>" if trades['trades_count'] > 0 else ''))
1132
        self._send_msg(message, parse_mode=ParseMode.HTML)
1✔
1133

1134
    @authorized_only
1✔
1135
    def _delete_trade(self, update: Update, context: CallbackContext) -> None:
1✔
1136
        """
1137
        Handler for /delete <id>.
1138
        Delete the given trade
1139
        :param bot: telegram bot
1140
        :param update: message update
1141
        :return: None
1142
        """
1143
        if not context.args or len(context.args) == 0:
1✔
1144
            raise RPCException("Trade-id not set.")
1✔
1145
        trade_id = int(context.args[0])
1✔
1146
        msg = self._rpc._rpc_delete(trade_id)
1✔
1147
        self._send_msg((
1✔
1148
            f"`{msg['result_msg']}`\n"
1149
            'Please make sure to take care of this asset on the exchange manually.'
1150
        ))
1151

1152
    @authorized_only
1✔
1153
    def _performance(self, update: Update, context: CallbackContext) -> None:
1✔
1154
        """
1155
        Handler for /performance.
1156
        Shows a performance statistic from finished trades
1157
        :param bot: telegram bot
1158
        :param update: message update
1159
        :return: None
1160
        """
1161
        trades = self._rpc._rpc_performance()
1✔
1162
        output = "<b>Performance:</b>\n"
1✔
1163
        for i, trade in enumerate(trades):
1✔
1164
            stat_line = (
1✔
1165
                f"{i+1}.\t <code>{trade['pair']}\t"
1166
                f"{round_coin_value(trade['profit_abs'], self._config['stake_currency'])} "
1167
                f"({trade['profit_ratio']:.2%}) "
1168
                f"({trade['count']})</code>\n")
1169

1170
            if len(output + stat_line) >= MAX_MESSAGE_LENGTH:
1✔
1171
                self._send_msg(output, parse_mode=ParseMode.HTML)
×
1172
                output = stat_line
×
1173
            else:
1174
                output += stat_line
1✔
1175

1176
        self._send_msg(output, parse_mode=ParseMode.HTML,
1✔
1177
                       reload_able=True, callback_path="update_performance",
1178
                       query=update.callback_query)
1179

1180
    @authorized_only
1✔
1181
    def _enter_tag_performance(self, update: Update, context: CallbackContext) -> None:
1✔
1182
        """
1183
        Handler for /buys PAIR .
1184
        Shows a performance statistic from finished trades
1185
        :param bot: telegram bot
1186
        :param update: message update
1187
        :return: None
1188
        """
1189
        pair = None
1✔
1190
        if context.args and isinstance(context.args[0], str):
1✔
1191
            pair = context.args[0]
1✔
1192

1193
        trades = self._rpc._rpc_enter_tag_performance(pair)
1✔
1194
        output = "<b>Entry Tag Performance:</b>\n"
1✔
1195
        for i, trade in enumerate(trades):
1✔
1196
            stat_line = (
1✔
1197
                f"{i+1}.\t <code>{trade['enter_tag']}\t"
1198
                f"{round_coin_value(trade['profit_abs'], self._config['stake_currency'])} "
1199
                f"({trade['profit_ratio']:.2%}) "
1200
                f"({trade['count']})</code>\n")
1201

1202
            if len(output + stat_line) >= MAX_MESSAGE_LENGTH:
1✔
1203
                self._send_msg(output, parse_mode=ParseMode.HTML)
×
1204
                output = stat_line
×
1205
            else:
1206
                output += stat_line
1✔
1207

1208
        self._send_msg(output, parse_mode=ParseMode.HTML,
1✔
1209
                       reload_able=True, callback_path="update_enter_tag_performance",
1210
                       query=update.callback_query)
1211

1212
    @authorized_only
1✔
1213
    def _exit_reason_performance(self, update: Update, context: CallbackContext) -> None:
1✔
1214
        """
1215
        Handler for /sells.
1216
        Shows a performance statistic from finished trades
1217
        :param bot: telegram bot
1218
        :param update: message update
1219
        :return: None
1220
        """
1221
        pair = None
1✔
1222
        if context.args and isinstance(context.args[0], str):
1✔
1223
            pair = context.args[0]
1✔
1224

1225
        trades = self._rpc._rpc_exit_reason_performance(pair)
1✔
1226
        output = "<b>Exit Reason Performance:</b>\n"
1✔
1227
        for i, trade in enumerate(trades):
1✔
1228
            stat_line = (
1✔
1229
                f"{i+1}.\t <code>{trade['exit_reason']}\t"
1230
                f"{round_coin_value(trade['profit_abs'], self._config['stake_currency'])} "
1231
                f"({trade['profit_ratio']:.2%}) "
1232
                f"({trade['count']})</code>\n")
1233

1234
            if len(output + stat_line) >= MAX_MESSAGE_LENGTH:
1✔
1235
                self._send_msg(output, parse_mode=ParseMode.HTML)
×
1236
                output = stat_line
×
1237
            else:
1238
                output += stat_line
1✔
1239

1240
        self._send_msg(output, parse_mode=ParseMode.HTML,
1✔
1241
                       reload_able=True, callback_path="update_exit_reason_performance",
1242
                       query=update.callback_query)
1243

1244
    @authorized_only
1✔
1245
    def _mix_tag_performance(self, update: Update, context: CallbackContext) -> None:
1✔
1246
        """
1247
        Handler for /mix_tags.
1248
        Shows a performance statistic from finished trades
1249
        :param bot: telegram bot
1250
        :param update: message update
1251
        :return: None
1252
        """
1253
        pair = None
1✔
1254
        if context.args and isinstance(context.args[0], str):
1✔
1255
            pair = context.args[0]
1✔
1256

1257
        trades = self._rpc._rpc_mix_tag_performance(pair)
1✔
1258
        output = "<b>Mix Tag Performance:</b>\n"
1✔
1259
        for i, trade in enumerate(trades):
1✔
1260
            stat_line = (
1✔
1261
                f"{i+1}.\t <code>{trade['mix_tag']}\t"
1262
                f"{round_coin_value(trade['profit_abs'], self._config['stake_currency'])} "
1263
                f"({trade['profit']:.2%}) "
1264
                f"({trade['count']})</code>\n")
1265

1266
            if len(output + stat_line) >= MAX_MESSAGE_LENGTH:
1✔
1267
                self._send_msg(output, parse_mode=ParseMode.HTML)
×
1268
                output = stat_line
×
1269
            else:
1270
                output += stat_line
1✔
1271

1272
        self._send_msg(output, parse_mode=ParseMode.HTML,
1✔
1273
                       reload_able=True, callback_path="update_mix_tag_performance",
1274
                       query=update.callback_query)
1275

1276
    @authorized_only
1✔
1277
    def _count(self, update: Update, context: CallbackContext) -> None:
1✔
1278
        """
1279
        Handler for /count.
1280
        Returns the number of trades running
1281
        :param bot: telegram bot
1282
        :param update: message update
1283
        :return: None
1284
        """
1285
        counts = self._rpc._rpc_count()
1✔
1286
        message = tabulate({k: [v] for k, v in counts.items()},
1✔
1287
                           headers=['current', 'max', 'total stake'],
1288
                           tablefmt='simple')
1289
        message = "<pre>{}</pre>".format(message)
1✔
1290
        logger.debug(message)
1✔
1291
        self._send_msg(message, parse_mode=ParseMode.HTML,
1✔
1292
                       reload_able=True, callback_path="update_count",
1293
                       query=update.callback_query)
1294

1295
    @authorized_only
1✔
1296
    def _locks(self, update: Update, context: CallbackContext) -> None:
1✔
1297
        """
1298
        Handler for /locks.
1299
        Returns the currently active locks
1300
        """
1301
        rpc_locks = self._rpc._rpc_locks()
1✔
1302
        if not rpc_locks['locks']:
1✔
1303
            self._send_msg('No active locks.', parse_mode=ParseMode.HTML)
1✔
1304

1305
        for locks in chunks(rpc_locks['locks'], 25):
1✔
1306
            message = tabulate([[
1✔
1307
                lock['id'],
1308
                lock['pair'],
1309
                lock['lock_end_time'],
1310
                lock['reason']] for lock in locks],
1311
                headers=['ID', 'Pair', 'Until', 'Reason'],
1312
                tablefmt='simple')
1313
            message = f"<pre>{escape(message)}</pre>"
1✔
1314
            logger.debug(message)
1✔
1315
            self._send_msg(message, parse_mode=ParseMode.HTML)
1✔
1316

1317
    @authorized_only
1✔
1318
    def _delete_locks(self, update: Update, context: CallbackContext) -> None:
1✔
1319
        """
1320
        Handler for /delete_locks.
1321
        Returns the currently active locks
1322
        """
1323
        arg = context.args[0] if context.args and len(context.args) > 0 else None
1✔
1324
        lockid = None
1✔
1325
        pair = None
1✔
1326
        if arg:
1✔
1327
            try:
1✔
1328
                lockid = int(arg)
1✔
1329
            except ValueError:
1✔
1330
                pair = arg
1✔
1331

1332
        self._rpc._rpc_delete_lock(lockid=lockid, pair=pair)
1✔
1333
        self._locks(update, context)
1✔
1334

1335
    @authorized_only
1✔
1336
    def _whitelist(self, update: Update, context: CallbackContext) -> None:
1✔
1337
        """
1338
        Handler for /whitelist
1339
        Shows the currently active whitelist
1340
        """
1341
        whitelist = self._rpc._rpc_whitelist()
1✔
1342

1343
        if context.args:
1✔
1344
            if "sorted" in context.args:
1✔
1345
                whitelist['whitelist'] = sorted(whitelist['whitelist'])
1✔
1346
            if "baseonly" in context.args:
1✔
1347
                whitelist['whitelist'] = [pair.split("/")[0] for pair in whitelist['whitelist']]
1✔
1348

1349
        message = f"Using whitelist `{whitelist['method']}` with {whitelist['length']} pairs\n"
1✔
1350
        message += f"`{', '.join(whitelist['whitelist'])}`"
1✔
1351

1352
        logger.debug(message)
1✔
1353
        self._send_msg(message)
1✔
1354

1355
    @authorized_only
1✔
1356
    def _blacklist(self, update: Update, context: CallbackContext) -> None:
1✔
1357
        """
1358
        Handler for /blacklist
1359
        Shows the currently active blacklist
1360
        """
1361
        self.send_blacklist_msg(self._rpc._rpc_blacklist(context.args))
1✔
1362

1363
    def send_blacklist_msg(self, blacklist: Dict):
1✔
1364
        errmsgs = []
1✔
1365
        for pair, error in blacklist['errors'].items():
1✔
1366
            errmsgs.append(f"Error adding `{pair}` to blacklist: `{error['error_msg']}`")
×
1367
        if errmsgs:
1✔
1368
            self._send_msg('\n'.join(errmsgs))
×
1369

1370
        message = f"Blacklist contains {blacklist['length']} pairs\n"
1✔
1371
        message += f"`{', '.join(blacklist['blacklist'])}`"
1✔
1372

1373
        logger.debug(message)
1✔
1374
        self._send_msg(message)
1✔
1375

1376
    @authorized_only
1✔
1377
    def _blacklist_delete(self, update: Update, context: CallbackContext) -> None:
1✔
1378
        """
1379
        Handler for /bl_delete
1380
        Deletes pair(s) from current blacklist
1381
        """
1382
        self.send_blacklist_msg(self._rpc._rpc_blacklist_delete(context.args or []))
1✔
1383

1384
    @authorized_only
1✔
1385
    def _logs(self, update: Update, context: CallbackContext) -> None:
1✔
1386
        """
1387
        Handler for /logs
1388
        Shows the latest logs
1389
        """
1390
        try:
1✔
1391
            limit = int(context.args[0]) if context.args else 10
1✔
1392
        except (TypeError, ValueError, IndexError):
×
1393
            limit = 10
×
1394
        logs = RPC._rpc_get_logs(limit)['logs']
1✔
1395
        msgs = ''
1✔
1396
        msg_template = "*{}* {}: {} \\- `{}`"
1✔
1397
        for logrec in logs:
1✔
1398
            msg = msg_template.format(escape_markdown(logrec[0], version=2),
1✔
1399
                                      escape_markdown(logrec[2], version=2),
1400
                                      escape_markdown(logrec[3], version=2),
1401
                                      escape_markdown(logrec[4], version=2))
1402
            if len(msgs + msg) + 10 >= MAX_MESSAGE_LENGTH:
1✔
1403
                # Send message immediately if it would become too long
1404
                self._send_msg(msgs, parse_mode=ParseMode.MARKDOWN_V2)
1✔
1405
                msgs = msg + '\n'
1✔
1406
            else:
1407
                # Append message to messages to send
1408
                msgs += msg + '\n'
1✔
1409

1410
        if msgs:
1✔
1411
            self._send_msg(msgs, parse_mode=ParseMode.MARKDOWN_V2)
1✔
1412

1413
    @authorized_only
1✔
1414
    def _edge(self, update: Update, context: CallbackContext) -> None:
1✔
1415
        """
1416
        Handler for /edge
1417
        Shows information related to Edge
1418
        """
1419
        edge_pairs = self._rpc._rpc_edge()
1✔
1420
        if not edge_pairs:
1✔
1421
            message = '<b>Edge only validated following pairs:</b>'
1✔
1422
            self._send_msg(message, parse_mode=ParseMode.HTML)
1✔
1423

1424
        for chunk in chunks(edge_pairs, 25):
1✔
1425
            edge_pairs_tab = tabulate(chunk, headers='keys', tablefmt='simple')
1✔
1426
            message = (f'<b>Edge only validated following pairs:</b>\n'
1✔
1427
                       f'<pre>{edge_pairs_tab}</pre>')
1428

1429
            self._send_msg(message, parse_mode=ParseMode.HTML)
1✔
1430

1431
    @authorized_only
1✔
1432
    def _help(self, update: Update, context: CallbackContext) -> None:
1✔
1433
        """
1434
        Handler for /help.
1435
        Show commands of the bot
1436
        :param bot: telegram bot
1437
        :param update: message update
1438
        :return: None
1439
        """
1440
        force_enter_text = ("*/forcelong <pair> [<rate>]:* `Instantly buys the given pair. "
1✔
1441
                            "Optionally takes a rate at which to buy "
1442
                            "(only applies to limit orders).` \n"
1443
                            )
1444
        if self._rpc._freqtrade.trading_mode != TradingMode.SPOT:
1✔
1445
            force_enter_text += ("*/forceshort <pair> [<rate>]:* `Instantly shorts the given pair. "
×
1446
                                 "Optionally takes a rate at which to sell "
1447
                                 "(only applies to limit orders).` \n")
1448
        message = (
1✔
1449
            "_Bot Control_\n"
1450
            "------------\n"
1451
            "*/start:* `Starts the trader`\n"
1452
            "*/stop:* Stops the trader\n"
1453
            "*/stopentry:* `Stops entering, but handles open trades gracefully` \n"
1454
            "*/forceexit <trade_id>|all:* `Instantly exits the given trade or all trades, "
1455
            "regardless of profit`\n"
1456
            "*/fx <trade_id>|all:* `Alias to /forceexit`\n"
1457
            f"{force_enter_text if self._config.get('force_entry_enable', False) else ''}"
1458
            "*/delete <trade_id>:* `Instantly delete the given trade in the database`\n"
1459
            "*/whitelist [sorted] [baseonly]:* `Show current whitelist. Optionally in "
1460
            "order and/or only displaying the base currency of each pairing.`\n"
1461
            "*/blacklist [pair]:* `Show current blacklist, or adds one or more pairs "
1462
            "to the blacklist.` \n"
1463
            "*/blacklist_delete [pairs]| /bl_delete [pairs]:* "
1464
            "`Delete pair / pattern from blacklist. Will reset on reload_conf.` \n"
1465
            "*/reload_config:* `Reload configuration file` \n"
1466
            "*/unlock <pair|id>:* `Unlock this Pair (or this lock id if it's numeric)`\n"
1467

1468
            "_Current state_\n"
1469
            "------------\n"
1470
            "*/show_config:* `Show running configuration` \n"
1471
            "*/locks:* `Show currently locked pairs`\n"
1472
            "*/balance:* `Show account balance per currency`\n"
1473
            "*/logs [limit]:* `Show latest logs - defaults to 10` \n"
1474
            "*/count:* `Show number of active trades compared to allowed number of trades`\n"
1475
            "*/edge:* `Shows validated pairs by Edge if it is enabled` \n"
1476
            "*/health* `Show latest process timestamp - defaults to 1970-01-01 00:00:00` \n"
1477

1478
            "_Statistics_\n"
1479
            "------------\n"
1480
            "*/status <trade_id>|[table]:* `Lists all open trades`\n"
1481
            "         *<trade_id> :* `Lists one or more specific trades.`\n"
1482
            "                        `Separate multiple <trade_id> with a blank space.`\n"
1483
            "         *table :* `will display trades in a table`\n"
1484
            "                `pending buy orders are marked with an asterisk (*)`\n"
1485
            "                `pending sell orders are marked with a double asterisk (**)`\n"
1486
            "*/buys <pair|none>:* `Shows the enter_tag performance`\n"
1487
            "*/sells <pair|none>:* `Shows the exit reason performance`\n"
1488
            "*/mix_tags <pair|none>:* `Shows combined entry tag + exit reason performance`\n"
1489
            "*/trades [limit]:* `Lists last closed trades (limited to 10 by default)`\n"
1490
            "*/profit [<n>]:* `Lists cumulative profit from all finished trades, "
1491
            "over the last n days`\n"
1492
            "*/performance:* `Show performance of each finished trade grouped by pair`\n"
1493
            "*/daily <n>:* `Shows profit or loss per day, over the last n days`\n"
1494
            "*/weekly <n>:* `Shows statistics per week, over the last n weeks`\n"
1495
            "*/monthly <n>:* `Shows statistics per month, over the last n months`\n"
1496
            "*/stats:* `Shows Wins / losses by Sell reason as well as "
1497
            "Avg. holding durations for buys and sells.`\n"
1498
            "*/help:* `This help message`\n"
1499
            "*/version:* `Show version`"
1500
            )
1501

1502
        self._send_msg(message, parse_mode=ParseMode.MARKDOWN)
1✔
1503

1504
    @authorized_only
1✔
1505
    def _health(self, update: Update, context: CallbackContext) -> None:
1✔
1506
        """
1507
        Handler for /health
1508
        Shows the last process timestamp
1509
        """
1510
        health = self._rpc._health()
×
1511
        message = f"Last process: `{health['last_process_loc']}`"
×
1512
        self._send_msg(message)
×
1513

1514
    @authorized_only
1✔
1515
    def _version(self, update: Update, context: CallbackContext) -> None:
1✔
1516
        """
1517
        Handler for /version.
1518
        Show version information
1519
        :param bot: telegram bot
1520
        :param update: message update
1521
        :return: None
1522
        """
1523
        strategy_version = self._rpc._freqtrade.strategy.version()
1✔
1524
        version_string = f'*Version:* `{__version__}`'
1✔
1525
        if strategy_version is not None:
1✔
1526
            version_string += f', *Strategy version: * `{strategy_version}`'
1✔
1527

1528
        self._send_msg(version_string)
1✔
1529

1530
    @authorized_only
1✔
1531
    def _show_config(self, update: Update, context: CallbackContext) -> None:
1✔
1532
        """
1533
        Handler for /show_config.
1534
        Show config information information
1535
        :param bot: telegram bot
1536
        :param update: message update
1537
        :return: None
1538
        """
1539
        val = RPC._rpc_show_config(self._config, self._rpc._freqtrade.state)
1✔
1540

1541
        if val['trailing_stop']:
1✔
1542
            sl_info = (
1✔
1543
                f"*Initial Stoploss:* `{val['stoploss']}`\n"
1544
                f"*Trailing stop positive:* `{val['trailing_stop_positive']}`\n"
1545
                f"*Trailing stop offset:* `{val['trailing_stop_positive_offset']}`\n"
1546
                f"*Only trail above offset:* `{val['trailing_only_offset_is_reached']}`\n"
1547
            )
1548

1549
        else:
1550
            sl_info = f"*Stoploss:* `{val['stoploss']}`\n"
1✔
1551

1552
        if val['position_adjustment_enable']:
1✔
1553
            pa_info = (
×
1554
                f"*Position adjustment:* On\n"
1555
                f"*Max enter position adjustment:* `{val['max_entry_position_adjustment']}`\n"
1556
            )
1557
        else:
1558
            pa_info = "*Position adjustment:* Off\n"
1✔
1559

1560
        self._send_msg(
1✔
1561
            f"*Mode:* `{'Dry-run' if val['dry_run'] else 'Live'}`\n"
1562
            f"*Exchange:* `{val['exchange']}`\n"
1563
            f"*Market: * `{val['trading_mode']}`\n"
1564
            f"*Stake per trade:* `{val['stake_amount']} {val['stake_currency']}`\n"
1565
            f"*Max open Trades:* `{val['max_open_trades']}`\n"
1566
            f"*Minimum ROI:* `{val['minimal_roi']}`\n"
1567
            f"*Entry strategy:* ```\n{json.dumps(val['entry_pricing'])}```\n"
1568
            f"*Exit strategy:* ```\n{json.dumps(val['exit_pricing'])}```\n"
1569
            f"{sl_info}"
1570
            f"{pa_info}"
1571
            f"*Timeframe:* `{val['timeframe']}`\n"
1572
            f"*Strategy:* `{val['strategy']}`\n"
1573
            f"*Current state:* `{val['state']}`"
1574
        )
1575

1576
    def _update_msg(self, query: CallbackQuery, msg: str, callback_path: str = "",
1✔
1577
                    reload_able: bool = False, parse_mode: str = ParseMode.MARKDOWN) -> None:
1578
        if reload_able:
1✔
1579
            reply_markup = InlineKeyboardMarkup([
1✔
1580
                [InlineKeyboardButton("Refresh", callback_data=callback_path)],
1581
            ])
1582
        else:
1583
            reply_markup = InlineKeyboardMarkup([[]])
1✔
1584
        msg += "\nUpdated: {}".format(datetime.now().ctime())
1✔
1585
        if not query.message:
1✔
1586
            return
×
1587
        chat_id = query.message.chat_id
1✔
1588
        message_id = query.message.message_id
1✔
1589

1590
        try:
1✔
1591
            self._updater.bot.edit_message_text(
1✔
1592
                chat_id=chat_id,
1593
                message_id=message_id,
1594
                text=msg,
1595
                parse_mode=parse_mode,
1596
                reply_markup=reply_markup
1597
            )
1598
        except BadRequest as e:
1✔
1599
            if 'not modified' in e.message.lower():
1✔
1600
                pass
1✔
1601
            else:
1602
                logger.warning('TelegramError: %s', e.message)
1✔
1603
        except TelegramError as telegram_err:
1✔
1604
            logger.warning('TelegramError: %s! Giving up on that message.', telegram_err.message)
1✔
1605

1606
    def _send_msg(self, msg: str, parse_mode: str = ParseMode.MARKDOWN,
1✔
1607
                  disable_notification: bool = False,
1608
                  keyboard: List[List[InlineKeyboardButton]] = None,
1609
                  callback_path: str = "",
1610
                  reload_able: bool = False,
1611
                  query: Optional[CallbackQuery] = None) -> None:
1612
        """
1613
        Send given markdown message
1614
        :param msg: message
1615
        :param bot: alternative bot
1616
        :param parse_mode: telegram parse mode
1617
        :return: None
1618
        """
1619
        reply_markup: Union[InlineKeyboardMarkup, ReplyKeyboardMarkup]
1620
        if query:
1✔
1621
            self._update_msg(query=query, msg=msg, parse_mode=parse_mode,
1✔
1622
                             callback_path=callback_path, reload_able=reload_able)
1623
            return
1✔
1624
        if reload_able and self._config['telegram'].get('reload', True):
1✔
1625
            reply_markup = InlineKeyboardMarkup([
×
1626
                [InlineKeyboardButton("Refresh", callback_data=callback_path)]])
1627
        else:
1628
            if keyboard is not None:
1✔
1629
                reply_markup = InlineKeyboardMarkup(keyboard, resize_keyboard=True)
×
1630
            else:
1631
                reply_markup = ReplyKeyboardMarkup(self._keyboard, resize_keyboard=True)
1✔
1632
        try:
1✔
1633
            try:
1✔
1634
                self._updater.bot.send_message(
1✔
1635
                    self._config['telegram']['chat_id'],
1636
                    text=msg,
1637
                    parse_mode=parse_mode,
1638
                    reply_markup=reply_markup,
1639
                    disable_notification=disable_notification,
1640
                )
1641
            except NetworkError as network_err:
1✔
1642
                # Sometimes the telegram server resets the current connection,
1643
                # if this is the case we send the message again.
1644
                logger.warning(
1✔
1645
                    'Telegram NetworkError: %s! Trying one more time.',
1646
                    network_err.message
1647
                )
1648
                self._updater.bot.send_message(
1✔
1649
                    self._config['telegram']['chat_id'],
1650
                    text=msg,
1651
                    parse_mode=parse_mode,
1652
                    reply_markup=reply_markup,
1653
                    disable_notification=disable_notification,
1654
                )
1655
        except TelegramError as telegram_err:
1✔
1656
            logger.warning(
1✔
1657
                'TelegramError: %s! Giving up on that message.',
1658
                telegram_err.message
1659
            )
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc