Repository: freqtrade/freqtrade

Issue:
Recommended to add TG commands: /profit long and /profit short
<!-- 
Note: this section will not show up in the issue.
Have you search for this feature before requesting it? It's highly likely that a similar request was already filed.
-->

## Describe your environment
(if applicable)

* Operating system: Ubuntu 24.04.2
* Python Version: 3.12.3
* CCXT version: 4.4.77
* Freqtrade Version: 2025.4


## Describe the enhancement

Recommended to add TG commands: `/profit long` and `/profit short`

*Explain the enhancement you would like*

Currently, the TG command `/profit` can be used to view the overall trading report. 

However, during trading, we sometimes need to check the long and short reports separately to understand which direction the market is currently favoring.

Could you provide the `/profit long` and `/profit short` commands for this purpose?

Thank you!


Discussion:
doesn't `/entries` , `/exits` - or `/mix_tags` (obviously assuming the tags in the strategy are set properly) provide you with this information already.

Also - the `/profit` command already has an argument (days) - overloading the same property with assumptions may not be a good idea (from a UX perspective).

What "could" work is having a  `/profit_long` and `/profit_short` command (separate - with the same argument as above) - though i don't really see a necessity for this if i'm honest.

If you think you require this - would you be open to create a Pull Request adding this functionality?
Hi, I'm working on this. I will be submitting a pull request soon.



Patch:
diff --git a/README.md b/README.md
index 7fa99d54b4d..ce45d376afc 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 <trade_id>|[table]`: Lists all or specific open trades.
 - `/profit [<n>]`: Lists cumulative profit from all finished trades, over the last n days.
+- `/profit_long [<n>]`: Lists cumulative profit from all finished long trades, over the last n days.
+- `/profit_short [<n>]`: Lists cumulative profit from all finished short trades, over the last n days.
 - `/forceexit <trade_id>|all`: Instantly exits the given trade (Ignoring `minimum_roi`).
 - `/fx <trade_id>|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/persistence/trade_model.py b/freqtrade/persistence/trade_model.py
index 51ec4abefdd..ee1f9d9ff6e 100644
--- a/freqtrade/persistence/trade_model.py
+++ b/freqtrade/persistence/trade_model.py
@@ -2090,32 +2090,34 @@ def get_mix_tag_performance(pair: str | None) -> list[dict[str, Any]]:
         return resp
 
     @staticmethod
-    def get_best_pair(start_date: datetime | None = None):
+    def get_best_pair(trade_filter: list | None = None):
         """
         Get best pair with closed trade.
         NOTE: Not supported in Backtesting.
         :returns: Tuple containing (pair, profit_sum)
         """
-        filters: list = [Trade.is_open.is_(False)]
-        if start_date:
-            filters.append(Trade.close_date >= start_date)
+        if not trade_filter:
+            trade_filter = []
+        trade_filter.append(Trade.is_open.is_(False))
 
-        pair_rates_query = Trade._generic_performance_query([Trade.pair], filters)
+        pair_rates_query = Trade._generic_performance_query([Trade.pair], trade_filter)
         best_pair = Trade.session.execute(pair_rates_query).first()
         # returns pair, profit_ratio, abs_profit, count
         return best_pair
 
     @staticmethod
-    def get_trading_volume(start_date: datetime | None = None) -> float:
+    def get_trading_volume(trade_filter: list | None = None) -> float:
         """
         Get Trade volume based on Orders
         NOTE: Not supported in Backtesting.
         :returns: Tuple containing (pair, profit_sum)
         """
-        filters = [Order.status == "closed"]
-        if start_date:
-            filters.append(Order.order_filled_date >= start_date)
+        if not trade_filter:
+            trade_filter = []
+        trade_filter.append(Order.status == "closed")
         trading_volume = Trade.session.execute(
-            select(func.sum(Order.cost).label("volume")).filter(*filters)
+            select(func.sum(Order.cost).label("volume"))
+            .join(Order._trade_live)
+            .filter(*trade_filter)
         ).scalar_one()
         return trading_volume or 0.0
diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py
index 5d8ffc1414f..868b10d7d41 100644
--- a/freqtrade/rpc/rpc.py
+++ b/freqtrade/rpc/rpc.py
@@ -34,7 +34,7 @@
 from freqtrade.exchange.exchange_utils import price_to_precision
 from freqtrade.ft_types import AnnotationType
 from freqtrade.loggers import bufferHandler
-from freqtrade.persistence import CustomDataWrapper, KeyValueStore, PairLocks, Trade
+from freqtrade.persistence import CustomDataWrapper, KeyValueStore, Order, PairLocks, Trade
 from freqtrade.persistence.models import PairLock, custom_data_rpc_wrapper
 from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
 from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
@@ -502,20 +502,13 @@ def trade_win_loss(trade):
         durations = {"wins": wins_dur, "draws": draws_dur, "losses": losses_dur}
         return {"exit_reasons": exit_reasons, "durations": durations}
 
-    def _rpc_trade_statistics(
-        self, stake_currency: str, fiat_display_currency: str, start_date: datetime | None = None
+    def _collect_trade_statistics_data(
+        self,
+        trades: Sequence["Trade"],
+        stake_currency: str,
+        fiat_display_currency: str,
     ) -> dict[str, Any]:
-        """Returns cumulative profit statistics"""
-
-        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)
-        trades: Sequence[Trade] = Trade.session.scalars(
-            Trade.get_trades_query(trade_filter, include_orders=False).order_by(Trade.id)
-        ).all()
-
+        """Iterate trades, calculate various statistics, and return intermediate results."""
         profit_all_coin = []
         profit_all_ratio = []
         profit_closed_coin = []
@@ -544,7 +537,7 @@ def _rpc_trade_statistics(
                     losing_trades += 1
                     losing_profit += profit_abs
             else:
-                # Get current rate
+                # Get current rate for open trades
                 if len(trade.select_filled_orders(trade.entry_side)) == 0:
                     # Skip trades with no filled orders
                     continue
@@ -558,17 +551,74 @@ def _rpc_trade_statistics(
                     profit_abs = nan
                 else:
                     _profit = trade.calculate_profit(trade.close_rate or current_rate)
-
                     profit_ratio = _profit.profit_ratio
                     profit_abs = _profit.total_profit
 
             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 == "long":
+            dir_filter = Trade.is_short.is_(False)
+            trade_filter = trade_filter & dir_filter
+        elif direction == "short":
+            dir_filter = Trade.is_short.is_(True)
+            trade_filter = trade_filter & dir_filter
+
+        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)
+        best_pair_filters = [Trade.close_date > start_date]
+        trading_volume_filters = [Order.order_filled_date >= start_date]
+
+        if direction:
+            best_pair_filters.append(dir_filter)
+            trading_volume_filters.append(dir_filter)
+
+        best_pair = Trade.get_best_pair(best_pair_filters)
+        trading_volume = Trade.get_trading_volume(trading_volume_filters)
 
         # Prepare data to display
         profit_closed_coin_sum = round(sum(profit_closed_coin), 8)
diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py
index 7b39671930d..b93e3ec25d5 100644
--- a/freqtrade/rpc/telegram.py
+++ b/freqtrade/rpc/telegram.py
@@ -191,8 +191,8 @@ def _init_keyboard(self) -> None:
             r"/mix_tags",
             r"/daily$",
             r"/daily \d+$",
-            r"/profit$",
-            r"/profit \d+",
+            r"/profit([_ ]long|[_ ]short)?$",
+            r"/profit([_ ]long|[_ ]short)? \d+$",
             r"/stats$",
             r"/count$",
             r"/locks$",
@@ -305,13 +305,17 @@ def _init(self) -> None:
             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"),
             CallbackQueryHandler(self._daily, pattern="update_daily"),
             CallbackQueryHandler(self._weekly, pattern="update_weekly"),
             CallbackQueryHandler(self._monthly, pattern="update_monthly"),
-            CallbackQueryHandler(self._profit, pattern="update_profit"),
+            CallbackQueryHandler(self._profit_long, pattern="update_profit_long"),
+            CallbackQueryHandler(self._profit_short, pattern="update_profit_short"),
+            CallbackQueryHandler(self._profit, pattern=r"update_profit$"),
             CallbackQueryHandler(self._balance, pattern="update_balance"),
             CallbackQueryHandler(self._performance, pattern="update_performance"),
             CallbackQueryHandler(
@@ -995,29 +999,25 @@ async def _monthly(self, update: Update, context: CallbackContext) -> None:
         """
         await self._timeunit_stats(update, context, "months")
 
-    @authorized_only
-    async def _profit(self, update: Update, context: CallbackContext) -> None:
-        """
-        Handler for /profit.
-        Returns a cumulative profit statistics.
-        :param bot: telegram bot
-        :param update: message update
-        :return: None
+    def _format_profit_message(
+        self,
+        stats: dict,
+        stake_cur: str,
+        fiat_disp_cur: str,
+        timescale: int | None = None,
+        direction: str | None = None,
+    ) -> str:
         """
-        stake_cur = self._config["stake_currency"]
-        fiat_disp_cur = self._config.get("fiat_display_currency", "")
+        Format profit statistics message for telegram.
 
-        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)
+        :param stats: Trade statistics dictionary
+        :param stake_cur: Stake currency
+        :param fiat_disp_cur: Fiat display currency
+        :param timescale: Optional timescale filter
+        :param direction: Optional direction filter ('long', 'short', or None for all)
+        :return: Formatted markdown message
+        """
+        # Extract common variables
         profit_closed_coin = stats["profit_closed_coin"]
         profit_closed_ratio_mean = stats["profit_closed_ratio_mean"]
         profit_closed_percent = stats["profit_closed_percent"]
@@ -1037,66 +1037,153 @@ async def _profit(self, update: Update, context: CallbackContext) -> None:
         expectancy = stats["expectancy"]
         expectancy_ratio = stats["expectancy_ratio"]
 
+        # Direction-specific labels
+        direction_label = f" {direction}" if direction else ""
+        no_trades_msg = (
+            f"No{direction_label} trades yet.\n*Bot started:* `{stats['bot_start_date']}`"
+        )
+        no_closed_msg = f"`No closed{direction_label} trade` \n"
+        closed_roi_label = f"*ROI:* Closed{direction_label} trades"
+        all_roi_label = f"*ROI:* All{direction_label} trades"
+
         if stats["trade_count"] == 0:
-            markdown_msg = f"No trades yet.\n*Bot started:* `{stats['bot_start_date']}`"
-        else:
-            # Message to display
-            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 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"
-                    f"{fiat_closed_trades}"
-                )
-            else:
-                markdown_msg = "`No closed trade` \n"
-            fiat_all_trades = (
-                f"∙ `{fmt_coin(profit_all_fiat, fiat_disp_cur)}`\n" if fiat_disp_cur else ""
+            return no_trades_msg
+
+        # Build message
+        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 = (
+                f"{closed_roi_label}\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"
+                f"{fiat_closed_trades}"
             )
+        else:
+            markdown_msg = no_closed_msg
+
+        fiat_all_trades = (
+            f"∙ `{fmt_coin(profit_all_fiat, fiat_disp_cur)}`\n" if fiat_disp_cur else ""
+        )
+        markdown_msg += (
+            f"{all_roi_label}\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"
+            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"*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"
-                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})`"
+                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"
             )
-            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"
-                )
+
+        return markdown_msg
+
+    async def _profit_handler(
+        self,
+        update: Update,
+        context: CallbackContext,
+        direction: str | None = None,
+    ) -> None:
+        """
+        Common handler for profit commands.
+
+        :param update: Telegram update
+        :param context: Callback context
+        :param direction: Trade direction filter ('long', 'short', or None)
+        :param callback_path: Callback path for message updates
+        """
+        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:
+                if not direction:
+                    arg = context.args[0].lower()
+                    if arg in ("short", "long"):
+                        direction = arg
+                        context.args.pop(0)  # Remove direction from 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
+
+        # Get stats with optional direction filter
+        stats_kwargs = {
+            "stake_currency": stake_cur,
+            "fiat_display_currency": fiat_disp_cur,
+            "start_date": start_date,
+        }
+        if direction:
+            stats_kwargs["direction"] = direction
+
+        stats = self._rpc._rpc_trade_statistics(**stats_kwargs)
+        markdown_msg = self._format_profit_message(
+            stats, stake_cur, fiat_disp_cur, timescale, direction
+        )
+
         await self._send_msg(
             markdown_msg,
             reload_able=True,
-            callback_path="update_profit",
+            callback_path="update_profit" if not direction else f"update_profit_{direction}",
             query=update.callback_query,
         )
 
+    @authorized_only
+    async def _profit(self, update: Update, context: CallbackContext) -> None:
+        """
+        Handler for /profit.
+        Returns a cumulative profit statistics.
+        :param bot: telegram bot
+        :param update: message update
+        :return: None
+        """
+        await self._profit_handler(update, context)
+
+    @authorized_only
+    async def _profit_long(self, update: Update, context: CallbackContext) -> None:
+        """
+        Handler for /profit_long.
+        Returns cumulative profit statistics for long trades.
+        """
+        await self._profit_handler(update, context, direction="long")
+
+    @authorized_only
+    async def _profit_short(self, update: Update, context: CallbackContext) -> None:
+        """
+        Handler for /profit_short.
+        Returns cumulative profit statistics for short trades.
+        """
+        await self._profit_handler(update, context, direction="short")
+
     @authorized_only
     async def _stats(self, update: Update, context: CallbackContext) -> None:
         """
@@ -1869,6 +1956,10 @@ async def _help(self, update: Update, context: CallbackContext) -> None:
             "*/trades [limit]:* `Lists last closed trades (limited to 10 by default)`\n"
             "*/profit [<n>]:* `Lists cumulative profit from all finished trades, "
             "over the last n days`\n"
+            "*/profit_long [<n>]:* `Lists cumulative profit from all finished long trades, "
+            "over the last n days`\n"
+            "*/profit_short [<n>]:* `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 <n>:* `Shows profit or loss per day, over the last n days`\n"
             "*/weekly <n>:* `Shows statistics per week, over the last n weeks`\n"


Test patch:
diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py
index 460352fffe5..2fb50139069 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)
@@ -943,7 +943,7 @@ async def test_telegram_profit_handle(
     trade.is_open = False
     Trade.commit()
 
-    context.args = [3]
+    context.args = ["3"]
     await telegram._profit(update=update, context=context)
     assert msg_mock.call_count == 1
     assert "*ROI:* Closed trades" in msg_mock.call_args_list[-1][0][0]
@@ -967,6 +967,92 @@ async def test_telegram_profit_handle(
     assert "*Trading volume:* `126 USDT`" in msg_mock.call_args_list[-1][0][0]
 
 
+@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()
+
+    # Test support with "/profit long"
+    context = MagicMock()
+    context.args = ["long"]
+    await telegram._profit(update=update, context=context)
+    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()
+
+    # Test support with "/profit short"
+    context = MagicMock()
+    context.args = ["short"]
+    await telegram._profit(update=update, context=context)
+    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
+
+
 @pytest.mark.parametrize("is_short", [True, False])
 async def test_telegram_stats(default_conf, update, ticker, fee, mocker, is_short) -> None:
     mocker.patch("freqtrade.rpc.rpc.CryptoToFiatConverter._find_price", return_value=15000.0)