From e9841910e9f6670bcca5aa41952b806590bf91cd Mon Sep 17 00:00:00 2001 From: Eugene Schava Date: Thu, 20 May 2021 00:33:33 +0300 Subject: [PATCH 01/10] day/week options for Telegram '/profit' command --- README.md | 2 +- docs/telegram-usage.md | 4 ++-- freqtrade/rpc/rpc.py | 5 +++-- freqtrade/rpc/telegram.py | 17 +++++++++++++---- 4 files changed, 19 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index ab9597a77..ada31b42b 100644 --- a/README.md +++ b/README.md @@ -123,7 +123,7 @@ Telegram is not mandatory. However, this is a great way to control your bot. Mor - `/stop`: Stops the trader. - `/stopbuy`: Stop entering new trades. - `/status |[table]`: Lists all or specific open trades. -- `/profit`: Lists cumulative profit from all finished trades +- `/profit [day]|[week]`: Lists cumulative profit from all finished trades - `/forcesell |all`: Instantly sells the given trade (Ignoring `minimum_roi`). - `/performance`: Show performance of each finished trade grouped by pair - `/balance`: Show account balance per currency. diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index 07f5fe7dd..c477921de 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -130,7 +130,7 @@ You can create your own keyboard in `config.json`: !!! Note "Supported Commands" Only the following commands are allowed. Command arguments are not supported! - `/start`, `/stop`, `/status`, `/status table`, `/trades`, `/profit`, `/performance`, `/daily`, `/stats`, `/count`, `/locks`, `/balance`, `/stopbuy`, `/reload_config`, `/show_config`, `/logs`, `/whitelist`, `/blacklist`, `/edge`, `/help`, `/version` + `/start`, `/stop`, `/status`, `/status table`, `/trades`, `/profit`, `/profit day`, `/profit week`, `/performance`, `/daily`, `/stats`, `/count`, `/locks`, `/balance`, `/stopbuy`, `/reload_config`, `/show_config`, `/logs`, `/whitelist`, `/blacklist`, `/edge`, `/help`, `/version` ## Telegram commands @@ -154,7 +154,7 @@ official commands. You can ask at any moment for help with `/help`. | `/count` | Displays number of trades used and available | `/locks` | Show currently locked pairs. | `/unlock ` | Remove the lock for this pair (or for this lock id). -| `/profit` | Display a summary of your profit/loss from close trades and some stats about your performance +| `/profit [day]|[week]` | Display a summary of your profit/loss from close trades and some stats about your performance | `/forcesell ` | Instantly sells the given trade (Ignoring `minimum_roi`). | `/forcesell all` | Instantly sells all open trades (Ignoring `minimum_roi`). | `/forcebuy [rate]` | Instantly buys the given pair. Rate is optional. (`forcebuy_enable` must be set to True) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 2c54d743e..7a09c1b8f 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -352,9 +352,10 @@ class RPC: return {'sell_reasons': sell_reasons, 'durations': durations} def _rpc_trade_statistics( - self, stake_currency: str, fiat_display_currency: str) -> Dict[str, Any]: + self, stake_currency: str, fiat_display_currency: str, + start_date: datetime = datetime.fromtimestamp(0)) -> Dict[str, Any]: """ Returns cumulative profit statistics """ - trades = Trade.get_trades().order_by(Trade.id).all() + trades = Trade.get_trades([Trade.open_date >= start_date]).order_by(Trade.id).all() profit_all_coin = [] profit_all_ratio = [] diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 2e288ee33..ee7493938 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -5,7 +5,7 @@ This module manage Telegram communication """ import json import logging -from datetime import timedelta +from datetime import datetime, timedelta from html import escape from itertools import chain from typing import Any, Callable, Dict, List, Union @@ -98,7 +98,8 @@ class Telegram(RPCHandler): # this needs refacoring of the whole telegram module (same # problem in _help()). valid_keys: List[str] = ['/start', '/stop', '/status', '/status table', - '/trades', '/profit', '/performance', '/daily', + '/trades', '/performance', '/daily', + '/profit', '/profit day', '/profit week', '/stats', '/count', '/locks', '/balance', '/stopbuy', '/reload_config', '/show_config', '/logs', '/whitelist', '/blacklist', '/edge', @@ -421,9 +422,17 @@ class Telegram(RPCHandler): stake_cur = self._config['stake_currency'] fiat_disp_cur = self._config.get('fiat_display_currency', '') + start_date = datetime.fromtimestamp(0) + if context.args: + if 'day' in context.args: + start_date = datetime.utcnow().date() + elif 'week' in context.args: + start_date = datetime.utcnow().date() - timedelta(days=7) + stats = self._rpc._rpc_trade_statistics( stake_cur, - fiat_disp_cur) + fiat_disp_cur, + start_date) profit_closed_coin = stats['profit_closed_coin'] profit_closed_percent_mean = stats['profit_closed_percent_mean'] profit_closed_percent_sum = stats['profit_closed_percent_sum'] @@ -901,7 +910,7 @@ class Telegram(RPCHandler): " `pending buy orders are marked with an asterisk (*)`\n" " `pending sell orders are marked with a double asterisk (**)`\n" "*/trades [limit]:* `Lists last closed trades (limited to 10 by default)`\n" - "*/profit:* `Lists cumulative profit from all finished trades`\n" + "*/profit [day]|[week]:* `Lists cumulative profit from all finished trades`\n" "*/forcesell |all:* `Instantly sells the given trade or all trades, " "regardless of profit`\n" f"{forcebuy_text if self._config.get('forcebuy_enable', False) else ''}" From 935ed36433608a2beb3dabc15274171a3eb93d29 Mon Sep 17 00:00:00 2001 From: Eugene Schava Date: Thu, 20 May 2021 01:10:22 +0300 Subject: [PATCH 02/10] day/week options for Telegram '/profit' command mypy fix --- freqtrade/rpc/telegram.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index ee7493938..ebd1eacb2 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -5,7 +5,7 @@ This module manage Telegram communication """ import json import logging -from datetime import datetime, timedelta +from datetime import datetime, date, timedelta from html import escape from itertools import chain from typing import Any, Callable, Dict, List, Union @@ -425,9 +425,9 @@ class Telegram(RPCHandler): start_date = datetime.fromtimestamp(0) if context.args: if 'day' in context.args: - start_date = datetime.utcnow().date() + start_date = datetime.combine(date.today(), datetime.min.time()) elif 'week' in context.args: - start_date = datetime.utcnow().date() - timedelta(days=7) + start_date = datetime.combine(date.today(), datetime.min.time()) - timedelta(days=7) stats = self._rpc._rpc_trade_statistics( stake_cur, From 336f4aa6a787817d9a33ece738dc88c0a0e43af0 Mon Sep 17 00:00:00 2001 From: Eugene Schava Date: Thu, 20 May 2021 08:17:08 +0300 Subject: [PATCH 03/10] day/week options for Telegram '/profit' command isort fix --- freqtrade/rpc/telegram.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index ebd1eacb2..19c520efa 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -5,7 +5,7 @@ This module manage Telegram communication """ import json import logging -from datetime import datetime, date, timedelta +from datetime import date, datetime, timedelta from html import escape from itertools import chain from typing import Any, Callable, Dict, List, Union From a965436cd6443f41ed0f906eed8b8a025de7e0ea Mon Sep 17 00:00:00 2001 From: Eugene Schava Date: Fri, 28 May 2021 10:17:26 +0300 Subject: [PATCH 04/10] day/week options for Telegram '/profit' command format changed to "/profit n" --- README.md | 2 +- docs/telegram-usage.md | 4 ++-- freqtrade/rpc/telegram.py | 19 ++++++++++--------- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index ada31b42b..2730ee85d 100644 --- a/README.md +++ b/README.md @@ -123,7 +123,7 @@ Telegram is not mandatory. However, this is a great way to control your bot. Mor - `/stop`: Stops the trader. - `/stopbuy`: Stop entering new trades. - `/status |[table]`: Lists all or specific open trades. -- `/profit [day]|[week]`: Lists cumulative profit from all finished trades +- `/profit []`: Lists cumulative profit from all finished trades, over the last n days. - `/forcesell |all`: Instantly sells the given trade (Ignoring `minimum_roi`). - `/performance`: Show performance of each finished trade grouped by pair - `/balance`: Show account balance per currency. diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index c477921de..0e6bae380 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -130,7 +130,7 @@ You can create your own keyboard in `config.json`: !!! Note "Supported Commands" Only the following commands are allowed. Command arguments are not supported! - `/start`, `/stop`, `/status`, `/status table`, `/trades`, `/profit`, `/profit day`, `/profit week`, `/performance`, `/daily`, `/stats`, `/count`, `/locks`, `/balance`, `/stopbuy`, `/reload_config`, `/show_config`, `/logs`, `/whitelist`, `/blacklist`, `/edge`, `/help`, `/version` + `/start`, `/stop`, `/status`, `/status table`, `/trades`, `/profit`, `/performance`, `/daily`, `/stats`, `/count`, `/locks`, `/balance`, `/stopbuy`, `/reload_config`, `/show_config`, `/logs`, `/whitelist`, `/blacklist`, `/edge`, `/help`, `/version` ## Telegram commands @@ -154,7 +154,7 @@ official commands. You can ask at any moment for help with `/help`. | `/count` | Displays number of trades used and available | `/locks` | Show currently locked pairs. | `/unlock ` | Remove the lock for this pair (or for this lock id). -| `/profit [day]|[week]` | Display a summary of your profit/loss from close trades and some stats about your performance +| `/profit []` | Display a summary of your profit/loss from close trades and some stats about your performance, over the last n days (all trades by default) | `/forcesell ` | Instantly sells the given trade (Ignoring `minimum_roi`). | `/forcesell all` | Instantly sells all open trades (Ignoring `minimum_roi`). | `/forcebuy [rate]` | Instantly buys the given pair. Rate is optional. (`forcebuy_enable` must be set to True) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 19c520efa..4be990b96 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -206,13 +206,14 @@ class Telegram(RPCHandler): msg['emoji'] = self._get_sell_emoji(msg) message = ("{emoji} *{exchange}:* Selling {pair} (#{trade_id})\n" + "*Profit:* `{profit_percent:.2f}%`\n" + "*Sell Reason:* `{sell_reason}`\n" + "*Duration:* `{duration} ({duration_min:.1f} min)`\n" "*Amount:* `{amount:.8f}`\n" "*Open Rate:* `{open_rate:.8f}`\n" "*Current Rate:* `{current_rate:.8f}`\n" - "*Close Rate:* `{limit:.8f}`\n" - "*Sell Reason:* `{sell_reason}`\n" - "*Duration:* `{duration} ({duration_min:.1f} min)`\n" - "*Profit:* `{profit_percent:.2f}%`").format(**msg) + "*Close Rate:* `{limit:.8f}`" + ).format(**msg) # Check if all sell properties are available. # This might not be the case if the message origin is triggered by /forcesell @@ -423,11 +424,11 @@ class Telegram(RPCHandler): fiat_disp_cur = self._config.get('fiat_display_currency', '') start_date = datetime.fromtimestamp(0) - if context.args: - if 'day' in context.args: - start_date = datetime.combine(date.today(), datetime.min.time()) - elif 'week' in context.args: - start_date = datetime.combine(date.today(), datetime.min.time()) - timedelta(days=7) + try: + timescale = int(context.args[0]) if context.args else None + start_date = datetime.combine(date.today(), datetime.min.time()) - timedelta(days=timescale) + except (TypeError, ValueError, IndexError): + pass stats = self._rpc._rpc_trade_statistics( stake_cur, From 4b5a9d8c497f2a093cd226dcdf9835de2e9550f2 Mon Sep 17 00:00:00 2001 From: Eugene Schava Date: Fri, 28 May 2021 14:43:57 +0300 Subject: [PATCH 05/10] day/week options for Telegram '/profit' command revert accidental changes --- freqtrade/rpc/telegram.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 4be990b96..ef13d25f0 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -206,14 +206,13 @@ class Telegram(RPCHandler): msg['emoji'] = self._get_sell_emoji(msg) message = ("{emoji} *{exchange}:* Selling {pair} (#{trade_id})\n" - "*Profit:* `{profit_percent:.2f}%`\n" - "*Sell Reason:* `{sell_reason}`\n" - "*Duration:* `{duration} ({duration_min:.1f} min)`\n" "*Amount:* `{amount:.8f}`\n" "*Open Rate:* `{open_rate:.8f}`\n" "*Current Rate:* `{current_rate:.8f}`\n" - "*Close Rate:* `{limit:.8f}`" - ).format(**msg) + "*Close Rate:* `{limit:.8f}`\n" + "*Sell Reason:* `{sell_reason}`\n" + "*Duration:* `{duration} ({duration_min:.1f} min)`\n" + "*Profit:* `{profit_percent:.2f}%`").format(**msg) # Check if all sell properties are available. # This might not be the case if the message origin is triggered by /forcesell From 36b68d3702e9497cd1a7bd36779d0ad46676f6dd Mon Sep 17 00:00:00 2001 From: Eugene Schava Date: Fri, 28 May 2021 14:46:22 +0300 Subject: [PATCH 06/10] day/week options for Telegram '/profit' command format changed to "/profit n" --- freqtrade/rpc/telegram.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index ef13d25f0..27dea30fd 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -910,7 +910,7 @@ class Telegram(RPCHandler): " `pending buy orders are marked with an asterisk (*)`\n" " `pending sell orders are marked with a double asterisk (**)`\n" "*/trades [limit]:* `Lists last closed trades (limited to 10 by default)`\n" - "*/profit [day]|[week]:* `Lists cumulative profit from all finished trades`\n" + "*/profit []:* `Lists cumulative profit from all finished trades, over the last n days`\n" "*/forcesell |all:* `Instantly sells the given trade or all trades, " "regardless of profit`\n" f"{forcebuy_text if self._config.get('forcebuy_enable', False) else ''}" From 012309a06a84d556b1cbece295b92a2585c9585e Mon Sep 17 00:00:00 2001 From: Eugene Schava Date: Fri, 28 May 2021 17:03:31 +0300 Subject: [PATCH 07/10] day/week options for Telegram '/profit' command fixed line lenght --- freqtrade/rpc/telegram.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 27dea30fd..b86f1b29c 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -425,7 +425,8 @@ class Telegram(RPCHandler): start_date = datetime.fromtimestamp(0) try: timescale = int(context.args[0]) if context.args else None - start_date = datetime.combine(date.today(), datetime.min.time()) - timedelta(days=timescale) + today_start = datetime.combine(date.today(), datetime.min.time()) + start_date = today_start - timedelta(days=timescale) except (TypeError, ValueError, IndexError): pass @@ -910,7 +911,8 @@ class Telegram(RPCHandler): " `pending buy orders are marked with an asterisk (*)`\n" " `pending sell orders are marked with a double asterisk (**)`\n" "*/trades [limit]:* `Lists last closed trades (limited to 10 by default)`\n" - "*/profit []:* `Lists cumulative profit from all finished trades, over the last n days`\n" + "*/profit []:* `Lists cumulative profit from all finished trades, " + "over the last n days`\n" "*/forcesell |all:* `Instantly sells the given trade or all trades, " "regardless of profit`\n" f"{forcebuy_text if self._config.get('forcebuy_enable', False) else ''}" From 14df243661f817fb7ade4155cbb91e97e3e89cc6 Mon Sep 17 00:00:00 2001 From: Eugene Schava Date: Fri, 28 May 2021 17:18:23 +0300 Subject: [PATCH 08/10] day/week options for Telegram '/profit' command mypy fix --- freqtrade/rpc/telegram.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index b86f1b29c..db709c556 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -424,9 +424,10 @@ class Telegram(RPCHandler): start_date = datetime.fromtimestamp(0) try: - timescale = int(context.args[0]) if context.args else None - today_start = datetime.combine(date.today(), datetime.min.time()) - start_date = today_start - timedelta(days=timescale) + if context.args: + timescale = int(context.args[0]) + today_start = datetime.combine(date.today(), datetime.min.time()) + start_date = today_start - timedelta(days=timescale) except (TypeError, ValueError, IndexError): pass From 313567d07d0616bd40bb5b785dbf61eeb8d40aea Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 29 May 2021 08:12:25 +0200 Subject: [PATCH 09/10] Support having numbers in custom keyboard --- freqtrade/rpc/telegram.py | 22 +++++++++++++--------- tests/rpc/test_rpc_telegram.py | 4 ++-- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index db709c556..6c7fa0493 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -5,6 +5,7 @@ This module manage Telegram communication """ import json import logging +import re from datetime import date, datetime, timedelta from html import escape from itertools import chain @@ -97,24 +98,27 @@ class Telegram(RPCHandler): # TODO: DRY! - its not good to list all valid cmds here. But otherwise # this needs refacoring of the whole telegram module (same # problem in _help()). - valid_keys: List[str] = ['/start', '/stop', '/status', '/status table', - '/trades', '/performance', '/daily', - '/profit', '/profit day', '/profit week', - '/stats', '/count', '/locks', '/balance', - '/stopbuy', '/reload_config', '/show_config', - '/logs', '/whitelist', '/blacklist', '/edge', - '/help', '/version'] + valid_keys: List[str] = [r'/start$', r'/stop$', r'/status$', r'/status table$', + r'/trades$', r'/performance$', r'/daily$', r'/daily \d+$', + r'/profit$', r'/profit \d+', + r'/stats$', r'/count$', r'/locks$', r'/balance$', + r'/stopbuy$', r'/reload_config$', r'/show_config$', + r'/logs$', r'/whitelist$', r'/blacklist$', r'/edge$', + r'/forcebuy$', r'/help$', r'/version$'] + # Create keys for generation + valid_keys_print = [k.replace('$', '') for k in valid_keys] # custom keyboard specified in config.json cust_keyboard = self._config['telegram'].get('keyboard', []) if cust_keyboard: + combined = "(" + ")|(".join(valid_keys) + ")" # check for valid shortcuts invalid_keys = [b for b in chain.from_iterable(cust_keyboard) - if b not in valid_keys] + if not re.match(combined, b)] if len(invalid_keys): err_msg = ('config.telegram.keyboard: Invalid commands for ' f'custom Telegram keyboard: {invalid_keys}' - f'\nvalid commands are: {valid_keys}') + f'\nvalid commands are: {valid_keys_print}') raise OperationalException(err_msg) else: self._keyboard = cust_keyboard diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 6008ede66..a9af498b7 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1568,7 +1568,7 @@ def test__send_msg_keyboard(default_conf, mocker, caplog) -> None: ['/count', '/start', '/stop', '/help']] default_keyboard = ReplyKeyboardMarkup(default_keys_list) - custom_keys_list = [['/daily', '/stats', '/balance', '/profit'], + custom_keys_list = [['/daily', '/stats', '/balance', '/profit', '/profit 5'], ['/count', '/start', '/reload_config', '/help']] custom_keyboard = ReplyKeyboardMarkup(custom_keys_list) @@ -1602,5 +1602,5 @@ def test__send_msg_keyboard(default_conf, mocker, caplog) -> None: used_keyboard = bot.send_message.call_args[1]['reply_markup'] assert used_keyboard == custom_keyboard assert log_has("using custom keyboard from config.json: " - "[['/daily', '/stats', '/balance', '/profit'], ['/count', " + "[['/daily', '/stats', '/balance', '/profit', '/profit 5'], ['/count', " "'/start', '/reload_config', '/help']]", caplog) From 3310a4502977a730b34fac79c2d657d631edc2f6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 8 Jun 2021 20:10:43 +0200 Subject: [PATCH 10/10] Change wording if limited lookback is used --- freqtrade/rpc/telegram.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 6c7fa0493..0a5125020 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -427,6 +427,7 @@ class Telegram(RPCHandler): fiat_disp_cur = self._config.get('fiat_display_currency', '') start_date = datetime.fromtimestamp(0) + timescale = None try: if context.args: timescale = int(context.args[0]) @@ -466,16 +467,18 @@ class Telegram(RPCHandler): else: markdown_msg = "`No closed trade` \n" - markdown_msg += (f"*ROI:* All trades\n" - f"∙ `{round_coin_value(profit_all_coin, stake_cur)} " - f"({profit_all_percent_mean:.2f}%) " - f"({profit_all_percent_sum} \N{GREEK CAPITAL LETTER SIGMA}%)`\n" - f"∙ `{round_coin_value(profit_all_fiat, fiat_disp_cur)}`\n" - f"*Total Trade Count:* `{trade_count}`\n" - f"*First Trade opened:* `{first_trade_date}`\n" - f"*Latest Trade opened:* `{latest_trade_date}\n`" - f"*Win / Loss:* `{stats['winning_trades']} / {stats['losing_trades']}`" - ) + markdown_msg += ( + f"*ROI:* All trades\n" + f"∙ `{round_coin_value(profit_all_coin, stake_cur)} " + f"({profit_all_percent_mean:.2f}%) " + f"({profit_all_percent_sum} \N{GREEK CAPITAL LETTER SIGMA}%)`\n" + f"∙ `{round_coin_value(profit_all_fiat, fiat_disp_cur)}`\n" + f"*Total Trade Count:* `{trade_count}`\n" + f"*{'First Trade opened' if not timescale else 'Showing Profit since'}:* " + f"`{first_trade_date}`\n" + f"*Latest Trade opened:* `{latest_trade_date}\n`" + f"*Win / Loss:* `{stats['winning_trades']} / {stats['losing_trades']}`" + ) if stats['closed_trade_count'] > 0: markdown_msg += (f"\n*Avg. Duration:* `{avg_duration}`\n" f"*Best Performing:* `{best_pair}: {best_rate:.2f}%`")