diff --git a/README.md b/README.md index 7fa99d54b..ce45d376a 100644 --- a/README.md +++ b/README.md @@ -146,6 +146,8 @@ Telegram is not mandatory. However, this is a great way to control your bot. Mor - `/stopentry`: Stop entering new trades. - `/status |[table]`: Lists all or specific open trades. - `/profit []`: Lists cumulative profit from all finished trades, over the last n days. +- `/profit_long []`: Lists cumulative profit from all finished long trades, over the last n days. +- `/profit_short []`: Lists cumulative profit from all finished short trades, over the last n days. - `/forceexit |all`: Instantly exits the given trade (Ignoring `minimum_roi`). - `/fx |all`: Alias to `/forceexit` - `/performance`: Show performance of each finished trade grouped by pair @@ -154,6 +156,7 @@ Telegram is not mandatory. However, this is a great way to control your bot. Mor - `/help`: Show help message. - `/version`: Show version. + ## Development branches The project is currently setup in two main branches: diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 2909a719c..b928b1ae7 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -502,17 +502,13 @@ class RPC: durations = {"wins": wins_dur, "draws": draws_dur, "losses": losses_dur} return {"exit_reasons": exit_reasons, "durations": durations} - def _process_trade_stats( + def _collect_trade_statistics_data( self, - trades: Sequence[Trade], + trades: Sequence['Trade'], stake_currency: str, fiat_display_currency: str, - start_date: datetime, ) -> dict[str, Any]: - """ - Processes a list of trades and returns the statistics. - Helper for _rpc_trade_statistics. - """ + """Iterate trades, calculate various statistics, and return intermediate results.""" profit_all_coin = [] profit_all_ratio = [] profit_closed_coin = [] @@ -541,7 +537,9 @@ class RPC: losing_trades += 1 losing_profit += profit_abs else: + # Get current rate for open trades if len(trade.select_filled_orders(trade.entry_side)) == 0: + # Skip trades with no filled orders continue try: current_rate = self._freqtrade.exchange.get_rate( @@ -559,12 +557,66 @@ class RPC: profit_all_coin.append(profit_abs) profit_all_ratio.append(profit_ratio) + return { + "profit_all_coin": profit_all_coin, + "profit_all_ratio": profit_all_ratio, + "profit_closed_coin": profit_closed_coin, + "profit_closed_ratio": profit_closed_ratio, + "durations": durations, + "winning_trades": winning_trades, + "losing_trades": losing_trades, + "winning_profit": winning_profit, + "losing_profit": losing_profit, + } + + def _rpc_trade_statistics( + self, + stake_currency: str, + fiat_display_currency: str, + start_date: datetime | None = None, + direction: str | None = None + ) -> dict[str, Any]: + """ + Returns cumulative profit statistics, with optional direction filter (long/short) + """ + start_date = datetime.fromtimestamp(0) if start_date is None else start_date + + trade_filter = ( + (Trade.is_open.is_(False) & (Trade.close_date >= start_date)) | Trade.is_open.is_(True) + ) + + if direction: + if direction == "long": + trade_filter = trade_filter & Trade.is_short.is_(False) + elif direction == "short": + trade_filter = trade_filter & Trade.is_short.is_(True) + + trades: Sequence[Trade] = Trade.session.scalars( + Trade.get_trades_query(trade_filter, include_orders=False).order_by(Trade.id) + ).all() + + stats = self._collect_trade_statistics_data(trades, stake_currency, fiat_display_currency) + + profit_all_coin = stats["profit_all_coin"] + profit_all_ratio = stats["profit_all_ratio"] + profit_closed_coin = stats["profit_closed_coin"] + profit_closed_ratio = stats["profit_closed_ratio"] + durations = stats["durations"] + winning_trades = stats["winning_trades"] + losing_trades = stats["losing_trades"] + winning_profit = stats["winning_profit"] + losing_profit = stats["losing_profit"] + closed_trade_count = len([t for t in trades if not t.is_open]) + best_pair = Trade.get_best_pair(start_date) trading_volume = Trade.get_trading_volume(start_date) + + # Prepare data to display profit_closed_coin_sum = round(sum(profit_closed_coin), 8) - profit_closed_ratio_sum = sum(profit_closed_ratio) if profit_closed_ratio else 0.0 profit_closed_ratio_mean = float(mean(profit_closed_ratio) if profit_closed_ratio else 0.0) + profit_closed_ratio_sum = sum(profit_closed_ratio) if profit_closed_ratio else 0.0 + profit_closed_fiat = ( self._fiat_converter.convert_amount( profit_closed_coin_sum, stake_currency, fiat_display_currency @@ -572,17 +624,22 @@ class RPC: if self._fiat_converter else 0 ) + profit_all_coin_sum = round(sum(profit_all_coin), 8) - profit_all_ratio_sum = sum(profit_all_ratio) if profit_all_ratio else 0.0 profit_all_ratio_mean = float(mean(profit_all_ratio) if profit_all_ratio else 0.0) + # Doing the sum is not right - overall profit needs to be based on initial capital + profit_all_ratio_sum = sum(profit_all_ratio) if profit_all_ratio else 0.0 starting_balance = self._freqtrade.wallets.get_starting_balance() profit_closed_ratio_fromstart = 0.0 profit_all_ratio_fromstart = 0.0 if starting_balance: profit_closed_ratio_fromstart = profit_closed_coin_sum / starting_balance profit_all_ratio_fromstart = profit_all_coin_sum / starting_balance + profit_factor = winning_profit / abs(losing_profit) if losing_profit else float("inf") + winrate = (winning_trades / closed_trade_count) if closed_trade_count > 0 else 0 + trades_df = DataFrame( [ { @@ -594,7 +651,9 @@ class RPC: if not trade.is_open and trade.close_date ] ) + expectancy, expectancy_ratio = calculate_expectancy(trades_df) + drawdown = DrawDownResult() if len(trades_df) > 0: try: @@ -605,7 +664,9 @@ class RPC: starting_balance=starting_balance, ) except ValueError: + # ValueError if no losing trade. pass + profit_all_fiat = ( self._fiat_converter.convert_amount( profit_all_coin_sum, stake_currency, fiat_display_currency @@ -613,6 +674,7 @@ class RPC: if self._fiat_converter else 0 ) + first_date = trades[0].open_date_utc if trades else None last_date = trades[-1].open_date_utc if trades else None num = float(len(durations) or 1) @@ -644,7 +706,7 @@ class RPC: "latest_trade_timestamp": dt_ts_def(last_date, 0), "avg_duration": str(timedelta(seconds=sum(durations) / num)).split(".")[0], "best_pair": best_pair[0] if best_pair else "", - "best_rate": round(best_pair[1] * 100, 2) if best_pair else 0, + "best_rate": round(best_pair[1] * 100, 2) if best_pair else 0, # Deprecated "best_pair_profit_ratio": best_pair[1] if best_pair else 0, "best_pair_profit_abs": best_pair[2] if best_pair else 0, "winning_trades": winning_trades, @@ -671,36 +733,6 @@ class RPC: "bot_start_date": format_date(bot_start), } - - - def _rpc_trade_statistics( - self, - stake_currency: str, - fiat_display_currency: str, - start_date: datetime | None = None, - direction: str | None = None, - ) -> dict[str, Any]: - """Returns cumulative profit statistics""" - start_date_filter = datetime.fromtimestamp(0) if start_date is None else start_date - - trade_filter = ( - Trade.is_open.is_(False) & (Trade.close_date >= start_date_filter) - ) | Trade.is_open.is_(True) - - if direction: - if direction == 'long': - trade_filter &= Trade.is_short.is_(False) - elif direction == 'short': - trade_filter &= Trade.is_short.is_(True) - - trades: Sequence[Trade] = Trade.session.scalars( - Trade.get_trades_query(trade_filter, include_orders=False).order_by(Trade.id) - ).all() - - return self._process_trade_stats( - trades, stake_currency, fiat_display_currency, start_date_filter - ) - def __balance_get_est_stake( self, coin: str, stake_currency: str, amount: float, balance: Wallet ) -> tuple[float, float]: diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 189fe0205..d94fe2851 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -305,6 +305,8 @@ class Telegram(RPCHandler): CommandHandler("order", self._order), CommandHandler("list_custom_data", self._list_custom_data), CommandHandler("tg_info", self._tg_info), + CommandHandler("profit_long", self._profit_long), + CommandHandler("profit_short", self._profit_short), ] callbacks = [ CallbackQueryHandler(self._status_table, pattern="update_status_table"), @@ -1009,23 +1011,15 @@ class Telegram(RPCHandler): start_date = datetime.fromtimestamp(0) timescale = None - direction: str | None = None - args = list(context.args) if context.args else [] - if args and isinstance(args[0], str) and args[0].lower() in ('long', 'short'): - direction = args[0].lower() - args.pop(0) - if args: - try: - if context.args: - timescale = int(context.args[0]) - 1 - today_start = datetime.combine(date.today(), datetime.min.time()) - start_date = today_start - timedelta(days=timescale) - except (TypeError, ValueError, IndexError): - pass + try: + if context.args: + timescale = int(context.args[0]) - 1 + today_start = datetime.combine(date.today(), datetime.min.time()) + start_date = today_start - timedelta(days=timescale) + except (TypeError, ValueError, IndexError): + pass - stats = self._rpc._rpc_trade_statistics( - stake_cur, fiat_disp_cur, start_date,direction=direction - ) + stats = self._rpc._rpc_trade_statistics(stake_cur, fiat_disp_cur, start_date) profit_closed_coin = stats["profit_closed_coin"] profit_closed_ratio_mean = stats["profit_closed_ratio_mean"] profit_closed_percent = stats["profit_closed_percent"] @@ -1053,11 +1047,8 @@ class Telegram(RPCHandler): fiat_closed_trades = ( f"∙ `{fmt_coin(profit_closed_fiat, fiat_disp_cur)}`\n" if fiat_disp_cur else "" ) - - direction_str = f"{direction.capitalize()} " if direction else "" - markdown_msg = ( - f"*ROI ({direction_str}Trades):* Closed trades\n" + "*ROI:* Closed trades\n" f"∙ `{fmt_coin(profit_closed_coin, stake_cur)} " f"({profit_closed_ratio_mean:.2%}) " f"({profit_closed_percent} \N{GREEK CAPITAL LETTER SIGMA}%)`\n" @@ -1068,9 +1059,8 @@ class Telegram(RPCHandler): fiat_all_trades = ( f"∙ `{fmt_coin(profit_all_fiat, fiat_disp_cur)}`\n" if fiat_disp_cur else "" ) - direction_str_all = f"{direction.capitalize()} " if direction else "" markdown_msg += ( - f"*ROI ({direction_str_all}Trades):* All trades\n" + f"*ROI:* All trades\n" f"∙ `{fmt_coin(profit_all_coin, stake_cur)} " f"({profit_all_ratio_mean:.2%}) " f"({profit_all_percent} \N{GREEK CAPITAL LETTER SIGMA}%)`\n" @@ -1109,6 +1099,208 @@ class Telegram(RPCHandler): query=update.callback_query, ) + @authorized_only + async def _profit_long(self, update: Update, context: CallbackContext) -> None: + """ + Handler for /profit_long. + Returns cumulative profit statistics for long trades. + """ + stake_cur = self._config["stake_currency"] + 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]) - 1 + today_start = datetime.combine(date.today(), datetime.min.time()) + start_date = today_start - timedelta(days=timescale) + except (TypeError, ValueError, IndexError): + pass + stats = self._rpc._rpc_trade_statistics( + stake_cur, + fiat_disp_cur, + start_date, + direction="long" + ) + + profit_closed_coin = stats["profit_closed_coin"] + profit_closed_ratio_mean = stats["profit_closed_ratio_mean"] + profit_closed_percent = stats["profit_closed_percent"] + profit_closed_fiat = stats["profit_closed_fiat"] + profit_all_coin = stats["profit_all_coin"] + profit_all_ratio_mean = stats["profit_all_ratio_mean"] + profit_all_percent = stats["profit_all_percent"] + profit_all_fiat = stats["profit_all_fiat"] + trade_count = stats["trade_count"] + first_trade_date = f"{stats['first_trade_humanized']} ({stats['first_trade_date']})" + latest_trade_date = f"{stats['latest_trade_humanized']} ({stats['latest_trade_date']})" + avg_duration = stats["avg_duration"] + best_pair = stats["best_pair"] + best_pair_profit_ratio = stats["best_pair_profit_ratio"] + best_pair_profit_abs = fmt_coin(stats["best_pair_profit_abs"], stake_cur) + winrate = stats["winrate"] + expectancy = stats["expectancy"] + expectancy_ratio = stats["expectancy_ratio"] + if stats["trade_count"] == 0: + markdown_msg = f"No long trades yet.\n*Bot started:* `{stats['bot_start_date']}`" + else: + if stats["closed_trade_count"] > 0: + fiat_closed_trades = ( + f"∙ `{fmt_coin(profit_closed_fiat, fiat_disp_cur)}`\n" if fiat_disp_cur else "" + ) + markdown_msg = ( + "*ROI: Closed long trades*\n" + f"∙ `{fmt_coin(profit_closed_coin, stake_cur)} " + f"({profit_closed_ratio_mean:.2%}) " + f"({profit_closed_percent} \u03A3%)`\n" + f"{fiat_closed_trades}" + ) + else: + markdown_msg = "`No closed long trade` \n" + fiat_all_trades = ( + f"∙ `{fmt_coin(profit_all_fiat, fiat_disp_cur)}`\n" if fiat_disp_cur else "" + ) + markdown_msg += ( + f"*ROI: All long trades\n" + f"∙ `{fmt_coin(profit_all_coin, stake_cur)} " + f"({profit_all_ratio_mean:.2%}) " + f"({profit_all_percent} \u03A3%)`\n" + f"{fiat_all_trades}" + f"*Total Trade Count:* `{trade_count}`\n" + f"*Bot started:* `{stats['bot_start_date']}`\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']}`\n" + f"*Winrate:* `{winrate:.2%}`\n" + f"*Expectancy (Ratio):* `{expectancy:.2f} ({expectancy_ratio:.2f})`" + ) + if stats["closed_trade_count"] > 0: + markdown_msg += ( + f"\n*Avg. Duration:* `{avg_duration}`\n" + f"*Best Performing:* `{best_pair}: {best_pair_profit_abs} " + f"({best_pair_profit_ratio:.2%})`\n" + f"*Trading volume:* `{fmt_coin(stats['trading_volume'], stake_cur)}`\n" + f"*Profit factor:* `{stats['profit_factor']:.2f}`\n" + f"*Max Drawdown:* `{stats['max_drawdown']:.2%} " + f"({fmt_coin(stats['max_drawdown_abs'], stake_cur)})`\n" + f" from `{stats['max_drawdown_start']} " + f"({fmt_coin(stats['drawdown_high'], stake_cur)})`\n" + f" to `{stats['max_drawdown_end']} " + f"({fmt_coin(stats['drawdown_low'], stake_cur)})`\n" + f"*Current Drawdown:* `{stats['current_drawdown']:.2%} " + f"({fmt_coin(stats['current_drawdown_abs'], stake_cur)})`\n" + f" from `{stats['current_drawdown_start']} " + f"({fmt_coin(stats['current_drawdown_high'], stake_cur)})`\n" + ) + await self._send_msg( + markdown_msg, + reload_able=True, + callback_path="update_profit_long", + query=update.callback_query, + ) + + @authorized_only + async def _profit_short(self, update: Update, context: CallbackContext) -> None: + """ + Handler for /profit_short. + Returns cumulative profit statistics for short trades. + """ + stake_cur = self._config["stake_currency"] + 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]) - 1 + today_start = datetime.combine(date.today(), datetime.min.time()) + start_date = today_start - timedelta(days=timescale) + except (TypeError, ValueError, IndexError): + pass + stats = self._rpc._rpc_trade_statistics( + stake_cur, + fiat_disp_cur, + start_date, + direction="short" + ) + + profit_closed_coin = stats["profit_closed_coin"] + profit_closed_ratio_mean = stats["profit_closed_ratio_mean"] + profit_closed_percent = stats["profit_closed_percent"] + profit_closed_fiat = stats["profit_closed_fiat"] + profit_all_coin = stats["profit_all_coin"] + profit_all_ratio_mean = stats["profit_all_ratio_mean"] + profit_all_percent = stats["profit_all_percent"] + profit_all_fiat = stats["profit_all_fiat"] + trade_count = stats["trade_count"] + first_trade_date = f"{stats['first_trade_humanized']} ({stats['first_trade_date']})" + latest_trade_date = f"{stats['latest_trade_humanized']} ({stats['latest_trade_date']})" + avg_duration = stats["avg_duration"] + best_pair = stats["best_pair"] + best_pair_profit_ratio = stats["best_pair_profit_ratio"] + best_pair_profit_abs = fmt_coin(stats["best_pair_profit_abs"], stake_cur) + winrate = stats["winrate"] + expectancy = stats["expectancy"] + expectancy_ratio = stats["expectancy_ratio"] + if stats["trade_count"] == 0: + markdown_msg = f"No short trades yet.\n*Bot started:* `{stats['bot_start_date']}`" + else: + if stats["closed_trade_count"] > 0: + fiat_closed_trades = ( + f"∙ `{fmt_coin(profit_closed_fiat, fiat_disp_cur)}`\n" if fiat_disp_cur else "" + ) + markdown_msg = ( + "*ROI: Closed short trades*\n" + f"∙ `{fmt_coin(profit_closed_coin, stake_cur)} " + f"({profit_closed_ratio_mean:.2%}) " + f"({profit_closed_percent} \u03A3%)`\n" + f"{fiat_closed_trades}" + ) + else: + markdown_msg = "`No closed short trade` \n" + fiat_all_trades = ( + f"∙ `{fmt_coin(profit_all_fiat, fiat_disp_cur)}`\n" if fiat_disp_cur else "" + ) + markdown_msg += ( + f"*ROI: All short trades\n" + f"∙ `{fmt_coin(profit_all_coin, stake_cur)} " + f"({profit_all_ratio_mean:.2%}) " + f"({profit_all_percent} \u03A3%)`\n" + f"{fiat_all_trades}" + f"*Total Trade Count:* `{trade_count}`\n" + f"*Bot started:* `{stats['bot_start_date']}`\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']}`\n" + f"*Winrate:* `{winrate:.2%}`\n" + f"*Expectancy (Ratio):* `{expectancy:.2f} ({expectancy_ratio:.2f})`" + ) + if stats["closed_trade_count"] > 0: + markdown_msg += ( + f"\n*Avg. Duration:* `{avg_duration}`\n" + f"*Best Performing:* `{best_pair}: {best_pair_profit_abs} " + f"({best_pair_profit_ratio:.2%})`\n" + f"*Trading volume:* `{fmt_coin(stats['trading_volume'], stake_cur)}`\n" + f"*Profit factor:* `{stats['profit_factor']:.2f}`\n" + f"*Max Drawdown:* `{stats['max_drawdown']:.2%} " + f"({fmt_coin(stats['max_drawdown_abs'], stake_cur)})`\n" + f" from `{stats['max_drawdown_start']} " + f"({fmt_coin(stats['drawdown_high'], stake_cur)})`\n" + f" to `{stats['max_drawdown_end']} " + f"({fmt_coin(stats['drawdown_low'], stake_cur)})`\n" + f"*Current Drawdown:* `{stats['current_drawdown']:.2%} " + f"({fmt_coin(stats['current_drawdown_abs'], stake_cur)})`\n" + f" from `{stats['current_drawdown_start']} " + f"({fmt_coin(stats['current_drawdown_high'], stake_cur)})`\n" + ) + await self._send_msg( + markdown_msg, + reload_able=True, + callback_path="update_profit_short", + query=update.callback_query, + ) + @authorized_only async def _stats(self, update: Update, context: CallbackContext) -> None: """ @@ -1879,8 +2071,12 @@ class Telegram(RPCHandler): "*/exits :* `Shows the exit reason performance`\n" "*/mix_tags :* `Shows combined entry tag + exit reason performance`\n" "*/trades [limit]:* `Lists last closed trades (limited to 10 by default)`\n" - "*/profit [long|short] []:* `Show profit from finished trades (last n days).`\n " - "`Optional filter: long or short.`\n" + "*/profit []:* `Lists cumulative profit from all finished trades, " + "over the last n days`\n" + "*/profit_long []:* `Lists cumulative profit from all finished long trades, " + "over the last n days`\n" + "*/profit_short []:* `Lists cumulative profit from all finished short trades, " + "over the last n days`\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" "*/weekly :* `Shows statistics per week, over the last n weeks`\n" @@ -2187,3 +2383,4 @@ class Telegram(RPCHandler): ) except TelegramError as telegram_err: logger.warning("TelegramError: %s! Giving up on that message.", telegram_err.message) + diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 79d8713ac..531b3046d 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -171,7 +171,7 @@ def test_telegram_init(default_conf, mocker, caplog) -> None: "['pause', 'stopbuy', 'stopentry'], ['whitelist'], ['blacklist'], " "['bl_delete', 'blacklist_delete'], " "['logs'], ['health'], ['help'], ['version'], ['marketdir'], " - "['order'], ['list_custom_data'], ['tg_info']]" + "['order'], ['list_custom_data'], ['tg_info'], ['profit_long'], ['profit_short']]" ) assert log_has(message_str, caplog) @@ -921,7 +921,7 @@ async def test_telegram_profit_handle( await telegram._profit(update=update, context=context) assert msg_mock.call_count == 1 assert "No closed trade" in msg_mock.call_args_list[-1][0][0] - assert "*ROI (Trades):* All trades" in msg_mock.call_args_list[-1][0][0] + assert "*ROI:* All trades" in msg_mock.call_args_list[-1][0][0] mocker.patch("freqtrade.wallets.Wallets.get_starting_balance", return_value=1000) assert ( "∙ `0.298 USDT (0.50%) (0.03 \N{GREEK CAPITAL LETTER SIGMA}%)`" @@ -946,13 +946,13 @@ async def test_telegram_profit_handle( context.args = [3] await telegram._profit(update=update, context=context) assert msg_mock.call_count == 1 - assert "*ROI (Trades):* Closed trades" in msg_mock.call_args_list[-1][0][0] + assert "*ROI:* Closed trades" in msg_mock.call_args_list[-1][0][0] assert ( "∙ `5.685 USDT (9.45%) (0.57 \N{GREEK CAPITAL LETTER SIGMA}%)`" in msg_mock.call_args_list[-1][0][0] ) assert "∙ `6.253 USD`" in msg_mock.call_args_list[-1][0][0] - assert "*ROI (Trades):* All trades" in msg_mock.call_args_list[-1][0][0] + assert "*ROI:* All trades" in msg_mock.call_args_list[-1][0][0] assert ( "∙ `5.685 USDT (9.45%) (0.57 \N{GREEK CAPITAL LETTER SIGMA}%)`" in msg_mock.call_args_list[-1][0][0] @@ -966,19 +966,6 @@ async def test_telegram_profit_handle( assert "*Expectancy (Ratio):*" in msg_mock.call_args_list[-1][0][0] assert "*Trading volume:* `126 USDT`" in msg_mock.call_args_list[-1][0][0] - msg_mock.reset_mock() - # Test /profit long - context.args = ["long"] - await telegram._profit(update=update, context=context) - assert msg_mock.call_count == 1 - assert "*ROI (Long Trades):* All trades" in msg_mock.call_args_list[-1][0][0] - - msg_mock.reset_mock() - # Test /profit short - context.args = ["short"] - await telegram._profit(update=update, context=context) - assert msg_mock.call_count == 1 - assert "No trades yet." in msg_mock.call_args_list[-1][0][0] @pytest.mark.parametrize("is_short", [True, False]) async def test_telegram_stats(default_conf, update, ticker, fee, mocker, is_short) -> None: @@ -2995,3 +2982,76 @@ async def test__tg_info(default_conf_usdt, mocker, update): content = context.bot.send_message.call_args[1]["text"] assert "Freqtrade Bot Info:\n" in content assert '"chat_id": "1235"' in content + + +@pytest.mark.asyncio +async def test_telegram_profit_long_short_handle( + default_conf_usdt, + update, + ticker_usdt, + fee, + mocker, +): + """ + Test the /profit_long and /profit_short commands to ensure the output content + is consistent with /profit, covering both no trades and trades present cases. + """ + + mocker.patch("freqtrade.rpc.rpc.CryptoToFiatConverter._find_price", return_value=1.1) + mocker.patch.multiple(EXMS, fetch_ticker=ticker_usdt, get_fee=fee) + telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf_usdt) + + # When there are no trades + await telegram._profit_long(update=update, context=MagicMock()) + assert msg_mock.call_count == 1 + assert "No long trades yet." in msg_mock.call_args_list[0][0][0] + msg_mock.reset_mock() + await telegram._profit_short(update=update, context=MagicMock()) + assert msg_mock.call_count == 1 + assert "No short trades yet." in msg_mock.call_args_list[0][0][0] + msg_mock.reset_mock() + + # When there are trades + create_mock_trades_usdt(fee) + + # Keep only long trades + for t in Trade.get_trades_proxy(): + t.is_short = False + Trade.commit() + await telegram._profit_long(update=update, context=MagicMock()) + msg = msg_mock.call_args_list[0][0][0] + assert "*ROI: Closed long trades*" in msg + assert "*ROI: All long trades" in msg + assert "*Total Trade Count:*" in msg + assert "*Winrate:*" in msg + assert "*Expectancy (Ratio):*" in msg + assert "*Best Performing:*" in msg + assert "*Profit factor:*" in msg + assert "*Max Drawdown:*" in msg + assert "*Current Drawdown:*" in msg + msg_mock.reset_mock() + + # Keep only short trades + for t in Trade.get_trades_proxy(): + t.is_short = True + Trade.commit() + await telegram._profit_short(update=update, context=MagicMock()) + msg = msg_mock.call_args_list[0][0][0] + assert "*ROI: Closed short trades*" in msg + assert "*ROI: All short trades" in msg + assert "*Total Trade Count:*" in msg + assert "*Winrate:*" in msg + assert "*Expectancy (Ratio):*" in msg + assert "*Best Performing:*" in msg + assert "*Profit factor:*" in msg + assert "*Max Drawdown:*" in msg + assert "*Current Drawdown:*" in msg + msg_mock.reset_mock() + + # Test parameter passing + context = MagicMock() + context.args = ["2"] + await telegram._profit_long(update=update, context=context) + assert msg_mock.call_count == 1 + await telegram._profit_short(update=update, context=context) + assert msg_mock.call_count == 2