Add /profit long and /profit short commands#2

# Added `/profit_long` and `/profit_short` Commands

Users can now use commands like:

- `/profit_long [<n>]`
- `/profit_short [<n>]`
- `/profit [<n>]`

---

## Key Changes Implemented

### `freqtrade/rpc/telegram.py`:

- The `_profit` command handler has been updated to robustly parse `long` or `short` as optional arguments.
  - **Translation:** The `_profit` command handler has been improved to reliably interpret `long` or `short` as optional parameters.

- The determined direction is passed to the RPC layer.
  - **Translation:** The direction determined (either `long` or `short`) is passed to the RPC layer.

- The `/help` command documentation is updated.
  - **Translation:** The documentation for the `/help` command has been updated accordingly.

---

### `freqtrade/rpc/rpc.py`:

- The `_rpc_trade_statistics` method now accepts a direction parameter.
  - **Translation:** The `_rpc_trade_statistics` method has been updated to accept a `direction` parameter.

- The method has been refactored into a main function and a `_process_trade_stats` helper function to reduce complexity and improve readability.
  - **Translation:** The method has been refactored into a main function and a helper function, `_process_trade_stats`, to reduce complexity and improve readability.

- The database query filter is dynamically modified to include a condition on `Trade.is_short` when a direction is provided.
  - **Translation:** The database query filter dynamically adjusts to include a condition on `Trade.is_short` when a direction is specified.

---

### `tests/rpc/test_rpc_telegram.py`:

- Existing tests for `_profit` have been updated to match the new message format.
  - **Translation:** Existing tests for the `_profit` function have been updated to match the new message format.

- New test cases have been added to specifically validate the `long` and `short` filtering functionality.
  - **Translation:** New test cases have been added to specifically validate the filtering functionality for `long` and `short` trades.

---

## Testing

- All local `pytest` tests pass successfully.
  - **Translation:** All local `pytest` tests have passed successfully.

- All `ruff` linter checks pass.
  - **Translation:** All `ruff` code checks have passed.

- As I do not have a full local deployment, I am relying on the CI pipeline for final validation.
  - **Translation:** Since I don't have a complete local deployment, I am relying on the CI pipeline for final validation.

---
This time, only a little AI was used :)
Except for the translation.
This commit is contained in:
qqqqqf
2025-07-15 19:15:04 +08:00
parent 583738040c
commit 19b57ad87e
4 changed files with 373 additions and 81 deletions

View File

@@ -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