From 28411da83eedd4d0441e1c8f5836d0f3a0864e39 Mon Sep 17 00:00:00 2001 From: Xu Wang Date: Tue, 22 Sep 2020 22:28:12 +0100 Subject: [PATCH 01/15] Add the telegram command function template. --- freqtrade/rpc/telegram.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index a01efaed6..a2dae387e 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -108,6 +108,7 @@ class Telegram(RPC): CommandHandler('edge', self._edge), CommandHandler('help', self._help), CommandHandler('version', self._version), + CommandHandler('stats', self._stats), ] for handle in handles: self._updater.dispatcher.add_handler(handle) @@ -738,6 +739,19 @@ class Telegram(RPC): """ self._send_msg('*Version:* `{}`'.format(__version__)) + @authorized_only + def _stats(self, update: Update, context: CallbackContext) -> None: + """ + Handler for /stats + https://github.com/freqtrade/freqtrade/issues/3783 + Show stats of recent trades + :param update: message update + :return: None + """ + # TODO: self._send_msg(...) + trades = self._rpc_trade_history(-1) + + @authorized_only def _show_config(self, update: Update, context: CallbackContext) -> None: """ From 44ad0f631c00ed00063fa61ebe3aefd6b5f736a1 Mon Sep 17 00:00:00 2001 From: Xu Wang Date: Sat, 26 Sep 2020 22:40:54 +0100 Subject: [PATCH 02/15] Summarize trade reason for telegram command /stats. --- freqtrade/rpc/telegram.py | 35 +++++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index a2dae387e..47e9d67dc 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -749,9 +749,40 @@ class Telegram(RPC): :return: None """ # TODO: self._send_msg(...) - trades = self._rpc_trade_history(-1) - + def trade_win_loss(trade): + if trade['profit_abs'] > 0: + return 'Wins' + elif trade['profit_abs'] < 0: + return 'Losses' + else: + return 'Draws' + trades = self._rpc_trade_history(-1) + trades_closed = [trade for trade in trades if not trade['is_open']] + + # Sell reason + sell_reasons = {} + for trade in trades_closed: + if trade['sell_reason'] in sell_reasons: + sell_reasons[trade['sell_reason']][trade_win_loss(trade)] += 1 + else: + win_loss_count = {'Wins': 0, 'Losses': 0, 'Draws': 0} + win_loss_count[trade_win_loss(trade)] += 1 + sell_reasons[trade['sell_reason']] = win_loss_count + sell_reason_msg = [ + '| Sell Reason | Sells | Wins | Draws | Losses |', + '|-------------|------:|-----:|------:|-------:|' + ] + # | Sell Reason | Sells | Wins | Draws | Losses | + # |-------------|------:|-----:|------:|-------:| + # | test | 1 | 2 | 3 | 4 | + for reason, count in sell_reasons.items(): + msg = f'| `{reason}` | `{sum(count.values())}` | `{count['Wins']}` | `{count['Draws']}` | `{count['Losses']}` |' + sell_reason_msg.append(msg) + + # TODO: Duration + + @authorized_only def _show_config(self, update: Update, context: CallbackContext) -> None: """ From 627e221b654e5b0ebf87d6f299f70e26c798e3b7 Mon Sep 17 00:00:00 2001 From: Xu Wang Date: Sun, 27 Sep 2020 20:23:13 +0100 Subject: [PATCH 03/15] Use tabulate to create sell reason message. --- freqtrade/rpc/telegram.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 47e9d67dc..ea8597469 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -769,16 +769,21 @@ class Telegram(RPC): win_loss_count = {'Wins': 0, 'Losses': 0, 'Draws': 0} win_loss_count[trade_win_loss(trade)] += 1 sell_reasons[trade['sell_reason']] = win_loss_count - sell_reason_msg = [ - '| Sell Reason | Sells | Wins | Draws | Losses |', - '|-------------|------:|-----:|------:|-------:|' - ] + sell_reasons_tabulate = [] # | Sell Reason | Sells | Wins | Draws | Losses | # |-------------|------:|-----:|------:|-------:| # | test | 1 | 2 | 3 | 4 | for reason, count in sell_reasons.items(): - msg = f'| `{reason}` | `{sum(count.values())}` | `{count['Wins']}` | `{count['Draws']}` | `{count['Losses']}` |' - sell_reason_msg.append(msg) + sell_reasons_tabulate.append([ + reason, sum(count.values()), + count['Wins'], + count['Draws'], + count['Losses'] + ]) + sell_reasons_msg = tabulate( + sell_reasons_tabulate, + headers=['Sell Reason', 'Sells', 'Wins', 'Draws', 'Losses'] + ) # TODO: Duration From 7bce2cd29daa65a8013d0f2f44fee817901b2465 Mon Sep 17 00:00:00 2001 From: Xu Wang Date: Mon, 28 Sep 2020 20:30:20 +0100 Subject: [PATCH 04/15] Add trade duration by win/loss. --- freqtrade/rpc/telegram.py | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index ea8597469..bfe486951 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -763,16 +763,10 @@ class Telegram(RPC): # Sell reason sell_reasons = {} for trade in trades_closed: - if trade['sell_reason'] in sell_reasons: - sell_reasons[trade['sell_reason']][trade_win_loss(trade)] += 1 - else: - win_loss_count = {'Wins': 0, 'Losses': 0, 'Draws': 0} - win_loss_count[trade_win_loss(trade)] += 1 - sell_reasons[trade['sell_reason']] = win_loss_count + if trade['sell_reason'] not in sell_reasons: + sell_reasons[trade['sell_reason']] = {'Wins': 0, 'Losses': 0, 'Draws': 0} + sell_reasons[trade['sell_reason']][trade_win_loss(trade)] += 1 sell_reasons_tabulate = [] - # | Sell Reason | Sells | Wins | Draws | Losses | - # |-------------|------:|-----:|------:|-------:| - # | test | 1 | 2 | 3 | 4 | for reason, count in sell_reasons.items(): sell_reasons_tabulate.append([ reason, sum(count.values()), @@ -785,9 +779,22 @@ class Telegram(RPC): headers=['Sell Reason', 'Sells', 'Wins', 'Draws', 'Losses'] ) - # TODO: Duration + # Duration + dur = {'Wins': [], 'Draws': [], 'Losses': []} + for trade in trades_closed: + if trade['close_date'] is not None and trade['open_date'] is not None: + trade_dur = arrow.get(trade['close_date']) - arrow.get(trade['open_date']) + dur[trade_win_loss(trade)].append(trade_dur) + wins_dur = sum(dur['Wins']) / len(dur['Wins']) if len(dur['Wins']) > 0 else 'N/A' + draws_dur = sum(dur['Draws']) / len(dur['Draws']) if len(dur['Draws']) > 0 else 'N/A' + losses_dur = sum(dur['Losses']) / len(dur['Losses']) if len(dur['Losses']) > 0 else 'N/A' + duration_msg = tabulate( + [['Wins', str(wins_dur)], ['Draws', str(draws_dur)], ['Losses', str(losses_dur)]], + headers=['', 'Duration'] + ) + + self._send_msg('\n'.join([sell_reasons_msg, duration_msg])) - @authorized_only def _show_config(self, update: Update, context: CallbackContext) -> None: """ From 355afc082e4619e7de11640bae4b6dfc8cc61f81 Mon Sep 17 00:00:00 2001 From: Xu Wang Date: Mon, 5 Oct 2020 10:05:15 +0100 Subject: [PATCH 05/15] Add command 'stats' in expected test output. --- tests/rpc/test_rpc_telegram.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 762780111..bcb9abc85 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -78,7 +78,8 @@ def test_telegram_init(default_conf, mocker, caplog) -> None: "['balance'], ['start'], ['stop'], ['forcesell'], ['forcebuy'], ['trades'], " "['delete'], ['performance'], ['daily'], ['count'], ['reload_config', " "'reload_conf'], ['show_config', 'show_conf'], ['stopbuy'], " - "['whitelist'], ['blacklist'], ['logs'], ['edge'], ['help'], ['version']]") + "['whitelist'], ['blacklist'], ['logs'], ['edge'], ['help'], ['version'], " + "['stats']]") assert log_has(message_str, caplog) From 1c27aaab724b781487a92f103363071d1276c18f Mon Sep 17 00:00:00 2001 From: Xu Wang Date: Sun, 18 Oct 2020 20:24:13 +0100 Subject: [PATCH 06/15] Declare type of 'dur'. --- freqtrade/rpc/telegram.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index bfe486951..8404625f1 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -6,7 +6,7 @@ This module manage Telegram communication import json import logging import arrow -from typing import Any, Callable, Dict +from typing import Any, Callable, Dict, List from tabulate import tabulate from telegram import ParseMode, ReplyKeyboardMarkup, Update @@ -780,7 +780,7 @@ class Telegram(RPC): ) # Duration - dur = {'Wins': [], 'Draws': [], 'Losses': []} + dur: Dict[str, List[int]] = {'Wins': [], 'Draws': [], 'Losses': []} for trade in trades_closed: if trade['close_date'] is not None and trade['open_date'] is not None: trade_dur = arrow.get(trade['close_date']) - arrow.get(trade['open_date']) From c556d1b37e6e6367e1c05362522e494df274275b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 5 Dec 2020 14:06:46 +0100 Subject: [PATCH 07/15] Make /stats working --- freqtrade/rpc/rpc.py | 4 ++++ freqtrade/rpc/telegram.py | 21 +++++++++++---------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 9ac271ba0..e17ee6b4f 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -275,6 +275,10 @@ class RPC: "trades_count": len(output) } + def _rpc_stats(self): + trades = trades = Trade.get_trades([Trade.is_open.is_(False)]) + return trades + def _rpc_trade_statistics( self, stake_currency: str, fiat_display_currency: str) -> Dict[str, Any]: """ Returns cumulative profit statistics """ diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 074a6367f..29d2c6a01 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -782,22 +782,22 @@ class Telegram(RPC): """ # TODO: self._send_msg(...) def trade_win_loss(trade): - if trade['profit_abs'] > 0: + if trade.close_profit_abs > 0: return 'Wins' - elif trade['profit_abs'] < 0: + elif trade.close_profit_abs < 0: return 'Losses' else: return 'Draws' - trades = self._rpc_trade_history(-1) - trades_closed = [trade for trade in trades if not trade['is_open']] + trades = self._rpc_stats() + trades_closed = [trade for trade in trades if not trade.is_open] # Sell reason sell_reasons = {} for trade in trades_closed: - if trade['sell_reason'] not in sell_reasons: - sell_reasons[trade['sell_reason']] = {'Wins': 0, 'Losses': 0, 'Draws': 0} - sell_reasons[trade['sell_reason']][trade_win_loss(trade)] += 1 + if trade.sell_reason not in sell_reasons: + sell_reasons[trade.sell_reason] = {'Wins': 0, 'Losses': 0, 'Draws': 0} + sell_reasons[trade.sell_reason][trade_win_loss(trade)] += 1 sell_reasons_tabulate = [] for reason, count in sell_reasons.items(): sell_reasons_tabulate.append([ @@ -814,8 +814,8 @@ class Telegram(RPC): # Duration dur: Dict[str, List[int]] = {'Wins': [], 'Draws': [], 'Losses': []} for trade in trades_closed: - if trade['close_date'] is not None and trade['open_date'] is not None: - trade_dur = arrow.get(trade['close_date']) - arrow.get(trade['open_date']) + if trade.close_date is not None and trade.open_date is not None: + trade_dur = (trade.close_date - trade.open_date).total_seconds() dur[trade_win_loss(trade)].append(trade_dur) wins_dur = sum(dur['Wins']) / len(dur['Wins']) if len(dur['Wins']) > 0 else 'N/A' draws_dur = sum(dur['Draws']) / len(dur['Draws']) if len(dur['Draws']) > 0 else 'N/A' @@ -824,8 +824,9 @@ class Telegram(RPC): [['Wins', str(wins_dur)], ['Draws', str(draws_dur)], ['Losses', str(losses_dur)]], headers=['', 'Duration'] ) + msg = (f"""```{sell_reasons_msg}```\n```{duration_msg}```""") - self._send_msg('\n'.join([sell_reasons_msg, duration_msg])) + self._send_msg(msg, ParseMode.MARKDOWN) @authorized_only def _show_config(self, update: Update, context: CallbackContext) -> None: From 143423145cacec63868390045c1e911390dba327 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 5 Dec 2020 14:38:42 +0100 Subject: [PATCH 08/15] Refactor most of the logic to rpc.py this way /stats can be used by other RPC methods too --- freqtrade/rpc/rpc.py | 31 +++++++++++++++++++- freqtrade/rpc/telegram.py | 59 ++++++++++++++++----------------------- 2 files changed, 54 insertions(+), 36 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index e17ee6b4f..d7a59390d 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -276,8 +276,37 @@ class RPC: } def _rpc_stats(self): + """ + Generate generic stats for trades in database + """ + def trade_win_loss(trade): + if trade.close_profit_abs > 0: + return 'Wins' + elif trade.close_profit_abs < 0: + return 'Losses' + else: + return 'Draws' trades = trades = Trade.get_trades([Trade.is_open.is_(False)]) - return trades + # Sell reason + sell_reasons = {} + for trade in trades: + if trade.sell_reason not in sell_reasons: + sell_reasons[trade.sell_reason] = {'Wins': 0, 'Losses': 0, 'Draws': 0} + sell_reasons[trade.sell_reason][trade_win_loss(trade)] += 1 + + # Duration + dur: Dict[str, List[int]] = {'Wins': [], 'Draws': [], 'Losses': []} + for trade in trades: + if trade.close_date is not None and trade.open_date is not None: + trade_dur = (trade.close_date - trade.open_date).total_seconds() + dur[trade_win_loss(trade)].append(trade_dur) + + wins_dur = sum(dur['Wins']) / len(dur['Wins']) if len(dur['Wins']) > 0 else 'N/A' + draws_dur = sum(dur['Draws']) / len(dur['Draws']) if len(dur['Draws']) > 0 else 'N/A' + losses_dur = sum(dur['Losses']) / len(dur['Losses']) if len(dur['Losses']) > 0 else 'N/A' + + durations = {'wins': wins_dur, 'draws': draws_dur, 'losses': losses_dur} + return sell_reasons, durations def _rpc_trade_statistics( self, stake_currency: str, fiat_display_currency: str) -> Dict[str, Any]: diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 29d2c6a01..7c7007f86 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -3,6 +3,7 @@ """ This module manage Telegram communication """ +from datetime import timedelta import json import logging from typing import Any, Callable, Dict, List, Union @@ -775,56 +776,44 @@ class Telegram(RPC): def _stats(self, update: Update, context: CallbackContext) -> None: """ Handler for /stats - https://github.com/freqtrade/freqtrade/issues/3783 Show stats of recent trades - :param update: message update :return: None """ - # TODO: self._send_msg(...) - def trade_win_loss(trade): - if trade.close_profit_abs > 0: - return 'Wins' - elif trade.close_profit_abs < 0: - return 'Losses' - else: - return 'Draws' + sell_reasons, durations = self._rpc_stats() - trades = self._rpc_stats() - trades_closed = [trade for trade in trades if not trade.is_open] - - # Sell reason - sell_reasons = {} - for trade in trades_closed: - if trade.sell_reason not in sell_reasons: - sell_reasons[trade.sell_reason] = {'Wins': 0, 'Losses': 0, 'Draws': 0} - sell_reasons[trade.sell_reason][trade_win_loss(trade)] += 1 sell_reasons_tabulate = [] + reason_map = { + 'roi': 'ROI', + 'stop_loss': 'Stoploss', + 'trailing_stop_loss': 'Trail. Stop', + 'stoploss_on_exchange': 'Stoploss', + 'sell_signal': 'Sell Signal', + 'force_sell': 'Forcesell', + 'emergency_sell': 'Emergency Sell', + } for reason, count in sell_reasons.items(): sell_reasons_tabulate.append([ - reason, sum(count.values()), + reason_map.get(reason, reason), + sum(count.values()), count['Wins'], - count['Draws'], + # count['Draws'], count['Losses'] ]) sell_reasons_msg = tabulate( sell_reasons_tabulate, - headers=['Sell Reason', 'Sells', 'Wins', 'Draws', 'Losses'] + headers=['Sell Reason', 'Sells', 'Wins', 'Losses'] ) - # Duration - dur: Dict[str, List[int]] = {'Wins': [], 'Draws': [], 'Losses': []} - for trade in trades_closed: - if trade.close_date is not None and trade.open_date is not None: - trade_dur = (trade.close_date - trade.open_date).total_seconds() - dur[trade_win_loss(trade)].append(trade_dur) - wins_dur = sum(dur['Wins']) / len(dur['Wins']) if len(dur['Wins']) > 0 else 'N/A' - draws_dur = sum(dur['Draws']) / len(dur['Draws']) if len(dur['Draws']) > 0 else 'N/A' - losses_dur = sum(dur['Losses']) / len(dur['Losses']) if len(dur['Losses']) > 0 else 'N/A' - duration_msg = tabulate( - [['Wins', str(wins_dur)], ['Draws', str(draws_dur)], ['Losses', str(losses_dur)]], - headers=['', 'Duration'] + duration_msg = tabulate([ + ['Wins', str(timedelta(seconds=durations['wins'])) + if durations['wins'] != 'N/A' else 'N/A'], + # ['Draws', str(timedelta(seconds=durations['draws']))], + ['Losses', str(timedelta(seconds=durations['losses'])) + if durations['losses'] != 'N/A' else 'N/A'] + ], + headers=['', 'Avg. Duration'] ) - msg = (f"""```{sell_reasons_msg}```\n```{duration_msg}```""") + msg = (f"""```\n{sell_reasons_msg}```\n```\n{duration_msg}```""") self._send_msg(msg, ParseMode.MARKDOWN) From aa27c9ace2fa3ac9b83780de5f1d0e4a9bd70fbf Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 5 Dec 2020 14:39:50 +0100 Subject: [PATCH 09/15] Reorder methods in telegram /stats is closely related to /profit --- freqtrade/rpc/telegram.py | 90 +++++++++++++++++++-------------------- 1 file changed, 45 insertions(+), 45 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 7c7007f86..76d9292b4 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -390,6 +390,51 @@ class Telegram(RPC): f"*Best Performing:* `{best_pair}: {best_rate:.2f}%`") self._send_msg(markdown_msg) + @authorized_only + def _stats(self, update: Update, context: CallbackContext) -> None: + """ + Handler for /stats + Show stats of recent trades + :return: None + """ + sell_reasons, durations = self._rpc_stats() + + sell_reasons_tabulate = [] + reason_map = { + 'roi': 'ROI', + 'stop_loss': 'Stoploss', + 'trailing_stop_loss': 'Trail. Stop', + 'stoploss_on_exchange': 'Stoploss', + 'sell_signal': 'Sell Signal', + 'force_sell': 'Forcesell', + 'emergency_sell': 'Emergency Sell', + } + for reason, count in sell_reasons.items(): + sell_reasons_tabulate.append([ + reason_map.get(reason, reason), + sum(count.values()), + count['Wins'], + # count['Draws'], + count['Losses'] + ]) + sell_reasons_msg = tabulate( + sell_reasons_tabulate, + headers=['Sell Reason', 'Sells', 'Wins', 'Losses'] + ) + + duration_msg = tabulate([ + ['Wins', str(timedelta(seconds=durations['wins'])) + if durations['wins'] != 'N/A' else 'N/A'], + # ['Draws', str(timedelta(seconds=durations['draws']))], + ['Losses', str(timedelta(seconds=durations['losses'])) + if durations['losses'] != 'N/A' else 'N/A'] + ], + headers=['', 'Avg. Duration'] + ) + msg = (f"""```\n{sell_reasons_msg}```\n```\n{duration_msg}```""") + + self._send_msg(msg, ParseMode.MARKDOWN) + @authorized_only def _balance(self, update: Update, context: CallbackContext) -> None: """ Handler for /balance """ @@ -772,51 +817,6 @@ class Telegram(RPC): """ self._send_msg('*Version:* `{}`'.format(__version__)) - @authorized_only - def _stats(self, update: Update, context: CallbackContext) -> None: - """ - Handler for /stats - Show stats of recent trades - :return: None - """ - sell_reasons, durations = self._rpc_stats() - - sell_reasons_tabulate = [] - reason_map = { - 'roi': 'ROI', - 'stop_loss': 'Stoploss', - 'trailing_stop_loss': 'Trail. Stop', - 'stoploss_on_exchange': 'Stoploss', - 'sell_signal': 'Sell Signal', - 'force_sell': 'Forcesell', - 'emergency_sell': 'Emergency Sell', - } - for reason, count in sell_reasons.items(): - sell_reasons_tabulate.append([ - reason_map.get(reason, reason), - sum(count.values()), - count['Wins'], - # count['Draws'], - count['Losses'] - ]) - sell_reasons_msg = tabulate( - sell_reasons_tabulate, - headers=['Sell Reason', 'Sells', 'Wins', 'Losses'] - ) - - duration_msg = tabulate([ - ['Wins', str(timedelta(seconds=durations['wins'])) - if durations['wins'] != 'N/A' else 'N/A'], - # ['Draws', str(timedelta(seconds=durations['draws']))], - ['Losses', str(timedelta(seconds=durations['losses'])) - if durations['losses'] != 'N/A' else 'N/A'] - ], - headers=['', 'Avg. Duration'] - ) - msg = (f"""```\n{sell_reasons_msg}```\n```\n{duration_msg}```""") - - self._send_msg(msg, ParseMode.MARKDOWN) - @authorized_only def _show_config(self, update: Update, context: CallbackContext) -> None: """ From 245c19f5e9aff5a797e0f1d71924d552b1f86a1c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 5 Dec 2020 14:48:56 +0100 Subject: [PATCH 10/15] Add simple test for /stats call --- freqtrade/rpc/rpc.py | 4 ++-- tests/conftest_trades.py | 2 ++ tests/rpc/test_rpc_telegram.py | 35 ++++++++++++++++++++++++++++++++++ 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index d7a59390d..c4b4117ff 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -280,9 +280,9 @@ class RPC: Generate generic stats for trades in database """ def trade_win_loss(trade): - if trade.close_profit_abs > 0: + if trade.close_profit > 0: return 'Wins' - elif trade.close_profit_abs < 0: + elif trade.close_profit < 0: return 'Losses' else: return 'Draws' diff --git a/tests/conftest_trades.py b/tests/conftest_trades.py index 78388f022..fac822b2b 100644 --- a/tests/conftest_trades.py +++ b/tests/conftest_trades.py @@ -82,6 +82,7 @@ def mock_trade_2(fee): is_open=False, open_order_id='dry_run_sell_12345', strategy='DefaultStrategy', + sell_reason='sell_signal' ) o = Order.parse_from_ccxt_object(mock_order_2(), 'ETC/BTC', 'buy') trade.orders.append(o) @@ -134,6 +135,7 @@ def mock_trade_3(fee): close_profit=0.01, exchange='bittrex', is_open=False, + sell_reason='roi' ) o = Order.parse_from_ccxt_object(mock_order_3(), 'XRP/BTC', 'buy') trade.orders.append(o) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 73a549860..725c1411e 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -469,6 +469,41 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee, assert '*Best Performing:* `ETH/BTC: 6.20%`' in msg_mock.call_args_list[-1][0][0] +def test_telegram_stats(default_conf, update, ticker, ticker_sell_up, fee, + limit_buy_order, limit_sell_order, mocker) -> None: + mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + fetch_ticker=ticker, + get_fee=fee, + ) + msg_mock = MagicMock() + mocker.patch.multiple( + 'freqtrade.rpc.telegram.Telegram', + _init=MagicMock(), + _send_msg=msg_mock + ) + + freqtradebot = get_patched_freqtradebot(mocker, default_conf) + patch_get_signal(freqtradebot, (True, False)) + telegram = Telegram(freqtradebot) + + telegram._stats(update=update, context=MagicMock()) + assert msg_mock.call_count == 1 + # assert 'No trades yet.' in msg_mock.call_args_list[0][0][0] + msg_mock.reset_mock() + + # Create some test data + create_mock_trades(fee) + + telegram._stats(update=update, context=MagicMock()) + assert msg_mock.call_count == 1 + assert 'Sell Reason' in msg_mock.call_args_list[-1][0][0] + assert 'ROI' in msg_mock.call_args_list[-1][0][0] + assert 'Avg. Duration' in msg_mock.call_args_list[-1][0][0] + msg_mock.reset_mock() + + def test_telegram_balance_handle(default_conf, update, mocker, rpc_balance, tickers) -> None: default_conf['dry_run'] = False mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value=rpc_balance) From e873cafdc49d46c2398550a77bd29dd61816a050 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 7 Dec 2020 14:54:39 +0100 Subject: [PATCH 11/15] Beautify code a bit --- freqtrade/rpc/rpc.py | 20 ++++++++++---------- freqtrade/rpc/telegram.py | 17 +++++++---------- 2 files changed, 17 insertions(+), 20 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index c4b4117ff..49e5bc2d2 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -275,38 +275,38 @@ class RPC: "trades_count": len(output) } - def _rpc_stats(self): + def _rpc_stats(self) -> Dict[str, Any]: """ Generate generic stats for trades in database """ def trade_win_loss(trade): if trade.close_profit > 0: - return 'Wins' + return 'wins' elif trade.close_profit < 0: - return 'Losses' + return 'losses' else: - return 'Draws' + return 'draws' trades = trades = Trade.get_trades([Trade.is_open.is_(False)]) # Sell reason sell_reasons = {} for trade in trades: if trade.sell_reason not in sell_reasons: - sell_reasons[trade.sell_reason] = {'Wins': 0, 'Losses': 0, 'Draws': 0} + sell_reasons[trade.sell_reason] = {'wins': 0, 'losses': 0, 'draws': 0} sell_reasons[trade.sell_reason][trade_win_loss(trade)] += 1 # Duration - dur: Dict[str, List[int]] = {'Wins': [], 'Draws': [], 'Losses': []} + dur: Dict[str, List[int]] = {'wins': [], 'draws': [], 'losses': []} for trade in trades: if trade.close_date is not None and trade.open_date is not None: trade_dur = (trade.close_date - trade.open_date).total_seconds() dur[trade_win_loss(trade)].append(trade_dur) - wins_dur = sum(dur['Wins']) / len(dur['Wins']) if len(dur['Wins']) > 0 else 'N/A' - draws_dur = sum(dur['Draws']) / len(dur['Draws']) if len(dur['Draws']) > 0 else 'N/A' - losses_dur = sum(dur['Losses']) / len(dur['Losses']) if len(dur['Losses']) > 0 else 'N/A' + wins_dur = sum(dur['wins']) / len(dur['wins']) if len(dur['wins']) > 0 else 'N/A' + draws_dur = sum(dur['draws']) / len(dur['draws']) if len(dur['draws']) > 0 else 'N/A' + losses_dur = sum(dur['losses']) / len(dur['losses']) if len(dur['losses']) > 0 else 'N/A' durations = {'wins': wins_dur, 'draws': draws_dur, 'losses': losses_dur} - return sell_reasons, durations + return {'sell_reasons': sell_reasons, 'durations': durations} def _rpc_trade_statistics( self, stake_currency: str, fiat_display_currency: str) -> Dict[str, Any]: diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 76d9292b4..25965e05f 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -3,9 +3,9 @@ """ This module manage Telegram communication """ -from datetime import timedelta import json import logging +from datetime import timedelta from typing import Any, Callable, Dict, List, Union import arrow @@ -395,9 +395,8 @@ class Telegram(RPC): """ Handler for /stats Show stats of recent trades - :return: None """ - sell_reasons, durations = self._rpc_stats() + stats = self._rpc_stats() sell_reasons_tabulate = [] reason_map = { @@ -409,26 +408,24 @@ class Telegram(RPC): 'force_sell': 'Forcesell', 'emergency_sell': 'Emergency Sell', } - for reason, count in sell_reasons.items(): + for reason, count in stats['sell_reasons'].items(): sell_reasons_tabulate.append([ reason_map.get(reason, reason), sum(count.values()), - count['Wins'], - # count['Draws'], - count['Losses'] + count['wins'], + count['losses'] ]) sell_reasons_msg = tabulate( sell_reasons_tabulate, headers=['Sell Reason', 'Sells', 'Wins', 'Losses'] ) - + durations = stats['durations'] duration_msg = tabulate([ ['Wins', str(timedelta(seconds=durations['wins'])) if durations['wins'] != 'N/A' else 'N/A'], - # ['Draws', str(timedelta(seconds=durations['draws']))], ['Losses', str(timedelta(seconds=durations['losses'])) if durations['losses'] != 'N/A' else 'N/A'] - ], + ], headers=['', 'Avg. Duration'] ) msg = (f"""```\n{sell_reasons_msg}```\n```\n{duration_msg}```""") From 81410fb4044c0d6238441ce86dcaed95d3d3e975 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 7 Dec 2020 15:03:16 +0100 Subject: [PATCH 12/15] Document /stats for telegram --- docs/telegram-usage.md | 1 + freqtrade/rpc/telegram.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index f4bd0a12a..c940f59ac 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -113,6 +113,7 @@ official commands. You can ask at any moment for help with `/help`. | `/performance` | Show performance of each finished trade grouped by pair | `/balance` | Show account balance per currency | `/daily ` | Shows profit or loss per day, over the last n days (n defaults to 7) +| `/stats` | Shows Wins / losses by Sell reason as well as Avg. holding durations for buys and sells | `/whitelist` | Show the current whitelist | `/blacklist [pair]` | Show the current blacklist, or adds a pair to the blacklist. | `/edge` | Show validated pairs by Edge if it is enabled. diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 25965e05f..b6c0a1f3f 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -787,6 +787,8 @@ class Telegram(RPC): "*/delete :* `Instantly delete the given trade in the database`\n" "*/performance:* `Show performance of each finished trade grouped by pair`\n" "*/daily :* `Shows profit or loss per day, over the last n days`\n" + "*/stats:* `Shows Wins / losses by Sell reason as well as " + "Avg. holding durationsfor buys and sells.`\n" "*/count:* `Show number of active trades compared to allowed number of trades`\n" "*/locks:* `Show currently locked pairs`\n" "*/balance:* `Show account balance per currency`\n" From 3ab5514697c294a9ab5918dc44e408f2f88bb341 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 7 Dec 2020 15:07:08 +0100 Subject: [PATCH 13/15] Add API endpoint for /stats --- docs/rest-api.md | 4 ++++ freqtrade/rpc/api_server.py | 14 ++++++++++++++ scripts/rest_client.py | 7 +++++++ tests/rpc/test_rpc_apiserver.py | 29 +++++++++++++++++++++++++++++ 4 files changed, 54 insertions(+) diff --git a/docs/rest-api.md b/docs/rest-api.md index 7726ab875..9bb35ce91 100644 --- a/docs/rest-api.md +++ b/docs/rest-api.md @@ -127,6 +127,7 @@ python3 scripts/rest_client.py --config rest_config.json [optional par | `performance` | Show performance of each finished trade grouped by pair. | `balance` | Show account balance per currency. | `daily ` | Shows profit or loss per day, over the last n days (n defaults to 7). +| `stats` | Display a summary of profit / loss reasons as well as average holding times. | `whitelist` | Show the current whitelist. | `blacklist [pair]` | Show the current blacklist, or adds a pair to the blacklist. | `edge` | Show validated pairs by Edge if it is enabled. @@ -229,6 +230,9 @@ show_config start Start the bot if it's in the stopped state. +stats + Return the stats report (durations, sell-reasons). + status Get the status of open trades. diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 8c2c203e6..c86aa1fa7 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -198,6 +198,8 @@ class ApiServer(RPC): self.app.add_url_rule(f'{BASE_URI}/logs', 'log', view_func=self._get_logs, methods=['GET']) self.app.add_url_rule(f'{BASE_URI}/profit', 'profit', view_func=self._profit, methods=['GET']) + self.app.add_url_rule(f'{BASE_URI}/stats', 'stats', + view_func=self._stats, methods=['GET']) self.app.add_url_rule(f'{BASE_URI}/performance', 'performance', view_func=self._performance, methods=['GET']) self.app.add_url_rule(f'{BASE_URI}/status', 'status', @@ -417,6 +419,18 @@ class ApiServer(RPC): return jsonify(stats) + @require_login + @rpc_catch_errors + def _stats(self): + """ + Handler for /stats. + Returns a Object with "durations" and "sell_reasons" as keys. + """ + + stats = self._rpc_stats() + + return jsonify(stats) + @require_login @rpc_catch_errors def _performance(self): diff --git a/scripts/rest_client.py b/scripts/rest_client.py index 268e81397..2232b8421 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -139,6 +139,13 @@ class FtRestClient(): """ return self._get("profit") + def stats(self): + """Return the stats report (durations, sell-reasons). + + :return: json object + """ + return self._get("stats") + def performance(self): """Return the performance of the different coins. diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 0dc43474f..2daa32bc7 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -559,6 +559,35 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, limit_buy_order, li } +@pytest.mark.usefixtures("init_persistence") +def test_api_stats(botclient, mocker, ticker, fee, markets,): + ftbot, client = botclient + patch_get_signal(ftbot, (True, False)) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_balances=MagicMock(return_value=ticker), + fetch_ticker=ticker, + get_fee=fee, + markets=PropertyMock(return_value=markets) + ) + + rc = client_get(client, f"{BASE_URI}/stats") + assert_response(rc, 200) + assert 'durations' in rc.json + assert 'sell_reasons' in rc.json + + create_mock_trades(fee) + + rc = client_get(client, f"{BASE_URI}/stats") + assert_response(rc, 200) + assert 'durations' in rc.json + assert 'sell_reasons' in rc.json + + assert 'wins' in rc.json['durations'] + assert 'losses' in rc.json['durations'] + assert 'draws' in rc.json['durations'] + + def test_api_performance(botclient, mocker, ticker, fee): ftbot, client = botclient patch_get_signal(ftbot, (True, False)) From 33f330256b02098458cbd18e28d970a735efb5e6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 9 Dec 2020 20:26:11 +0100 Subject: [PATCH 14/15] Reorder commands on telegram init --- freqtrade/rpc/telegram.py | 2 +- tests/conftest_trades.py | 10 ++++++++-- tests/rpc/test_rpc_telegram.py | 6 +++--- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index b6c0a1f3f..fa36cfee9 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -99,6 +99,7 @@ class Telegram(RPC): CommandHandler('trades', self._trades), CommandHandler('delete', self._delete_trade), CommandHandler('performance', self._performance), + CommandHandler('stats', self._stats), CommandHandler('daily', self._daily), CommandHandler('count', self._count), CommandHandler('locks', self._locks), @@ -111,7 +112,6 @@ class Telegram(RPC): CommandHandler('edge', self._edge), CommandHandler('help', self._help), CommandHandler('version', self._version), - CommandHandler('stats', self._stats), ] for handle in handles: self._updater.dispatcher.add_handler(handle) diff --git a/tests/conftest_trades.py b/tests/conftest_trades.py index fac822b2b..e84722041 100644 --- a/tests/conftest_trades.py +++ b/tests/conftest_trades.py @@ -1,3 +1,5 @@ +from datetime import datetime, timedelta, timezone + from freqtrade.persistence.models import Order, Trade @@ -82,7 +84,9 @@ def mock_trade_2(fee): is_open=False, open_order_id='dry_run_sell_12345', strategy='DefaultStrategy', - sell_reason='sell_signal' + sell_reason='sell_signal', + open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20), + close_date=datetime.now(tz=timezone.utc), ) o = Order.parse_from_ccxt_object(mock_order_2(), 'ETC/BTC', 'buy') trade.orders.append(o) @@ -135,7 +139,9 @@ def mock_trade_3(fee): close_profit=0.01, exchange='bittrex', is_open=False, - sell_reason='roi' + sell_reason='roi', + open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20), + close_date=datetime.now(tz=timezone.utc), ) o = Order.parse_from_ccxt_object(mock_order_3(), 'XRP/BTC', 'buy') trade.orders.append(o) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 725c1411e..ecad05683 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -74,10 +74,10 @@ def test_telegram_init(default_conf, mocker, caplog) -> None: message_str = ("rpc.telegram is listening for following commands: [['status'], ['profit'], " "['balance'], ['start'], ['stop'], ['forcesell'], ['forcebuy'], ['trades'], " - "['delete'], ['performance'], ['daily'], ['count'], ['locks'], " + "['delete'], ['performance'], ['stats'], ['daily'], ['count'], ['locks'], " "['reload_config', 'reload_conf'], ['show_config', 'show_conf'], ['stopbuy'], " - "['whitelist'], ['blacklist'], ['logs'], ['edge'], ['help'], ['version'], " - "['stats']]") + "['whitelist'], ['blacklist'], ['logs'], ['edge'], ['help'], ['version']" + "]") assert log_has(message_str, caplog) From ca99d484fcd852561f8acbb3bd9cbb879ddc724d Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 10 Dec 2020 07:39:50 +0100 Subject: [PATCH 15/15] Refactor to use list comprehension --- freqtrade/rpc/telegram.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index fa36cfee9..c54000677 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -398,7 +398,6 @@ class Telegram(RPC): """ stats = self._rpc_stats() - sell_reasons_tabulate = [] reason_map = { 'roi': 'ROI', 'stop_loss': 'Stoploss', @@ -408,13 +407,14 @@ class Telegram(RPC): 'force_sell': 'Forcesell', 'emergency_sell': 'Emergency Sell', } - for reason, count in stats['sell_reasons'].items(): - sell_reasons_tabulate.append([ + sell_reasons_tabulate = [ + [ reason_map.get(reason, reason), sum(count.values()), count['wins'], count['losses'] - ]) + ] for reason, count in stats['sell_reasons'].items() + ] sell_reasons_msg = tabulate( sell_reasons_tabulate, headers=['Sell Reason', 'Sells', 'Wins', 'Losses']