Modify the duplicate functions.

Modify the original three duplicate functions (_profit_short, _profit_long, _profit), and add _profit_handler and _format_profit_message.

Refactor telegram.py and rpc.py.

Sorry for the duplicate functions yesterday, I was a bit rushed.

Both pytest and ruff have passed.
This commit is contained in:
qqqqqf
2025-07-16 11:43:51 +08:00
parent 19b57ad87e
commit c92c64bac2
2 changed files with 165 additions and 283 deletions

View File

@@ -504,7 +504,7 @@ class RPC:
def _collect_trade_statistics_data( def _collect_trade_statistics_data(
self, self,
trades: Sequence['Trade'], trades: Sequence["Trade"],
stake_currency: str, stake_currency: str,
fiat_display_currency: str, fiat_display_currency: str,
) -> dict[str, Any]: ) -> dict[str, Any]:
@@ -574,7 +574,7 @@ class RPC:
stake_currency: str, stake_currency: str,
fiat_display_currency: str, fiat_display_currency: str,
start_date: datetime | None = None, start_date: datetime | None = None,
direction: str | None = None direction: str | None = None,
) -> dict[str, Any]: ) -> dict[str, Any]:
""" """
Returns cumulative profit statistics, with optional direction filter (long/short) Returns cumulative profit statistics, with optional direction filter (long/short)
@@ -582,8 +582,8 @@ class RPC:
start_date = datetime.fromtimestamp(0) if start_date is None else start_date start_date = datetime.fromtimestamp(0) if start_date is None else start_date
trade_filter = ( trade_filter = (
(Trade.is_open.is_(False) & (Trade.close_date >= start_date)) | Trade.is_open.is_(True) Trade.is_open.is_(False) & (Trade.close_date >= start_date)
) ) | Trade.is_open.is_(True)
if direction: if direction:
if direction == "long": if direction == "long":

View File

@@ -997,29 +997,25 @@ class Telegram(RPCHandler):
""" """
await self._timeunit_stats(update, context, "months") await self._timeunit_stats(update, context, "months")
@authorized_only def _format_profit_message(
async def _profit(self, update: Update, context: CallbackContext) -> None: self,
stats: dict,
stake_cur: str,
fiat_disp_cur: str,
timescale: int | None = None,
direction: str | None = None,
) -> str:
""" """
Handler for /profit. Format profit statistics message for telegram.
Returns a cumulative profit statistics.
:param bot: telegram bot :param stats: Trade statistics dictionary
:param update: message update :param stake_cur: Stake currency
:return: None :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
""" """
stake_cur = self._config["stake_currency"] # Extract common variables
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)
profit_closed_coin = stats["profit_closed_coin"] profit_closed_coin = stats["profit_closed_coin"]
profit_closed_ratio_mean = stats["profit_closed_ratio_mean"] profit_closed_ratio_mean = stats["profit_closed_ratio_mean"]
profit_closed_percent = stats["profit_closed_percent"] profit_closed_percent = stats["profit_closed_percent"]
@@ -1039,28 +1035,40 @@ class Telegram(RPCHandler):
expectancy = stats["expectancy"] expectancy = stats["expectancy"]
expectancy_ratio = stats["expectancy_ratio"] 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*" if direction else "*ROI:* Closed trades"
)
all_roi_label = f"*ROI: All{direction_label} trades" if direction else "*ROI:* All trades"
if stats["trade_count"] == 0: if stats["trade_count"] == 0:
markdown_msg = f"No trades yet.\n*Bot started:* `{stats['bot_start_date']}`" return no_trades_msg
else:
# Message to display # Build message
if stats["closed_trade_count"] > 0: if stats["closed_trade_count"] > 0:
fiat_closed_trades = ( fiat_closed_trades = (
f"∙ `{fmt_coin(profit_closed_fiat, fiat_disp_cur)}`\n" if fiat_disp_cur else "" f"∙ `{fmt_coin(profit_closed_fiat, fiat_disp_cur)}`\n" if fiat_disp_cur else ""
) )
markdown_msg = ( markdown_msg = (
"*ROI:* Closed trades\n" f"{closed_roi_label}\n"
f"∙ `{fmt_coin(profit_closed_coin, stake_cur)} " f"∙ `{fmt_coin(profit_closed_coin, stake_cur)} "
f"({profit_closed_ratio_mean:.2%}) " f"({profit_closed_ratio_mean:.2%}) "
f"({profit_closed_percent} \N{GREEK CAPITAL LETTER SIGMA}%)`\n" f"({profit_closed_percent} \N{GREEK CAPITAL LETTER SIGMA}%)`\n"
f"{fiat_closed_trades}" f"{fiat_closed_trades}"
) )
else: else:
markdown_msg = "`No closed trade` \n" markdown_msg = no_closed_msg
fiat_all_trades = ( fiat_all_trades = (
f"∙ `{fmt_coin(profit_all_fiat, fiat_disp_cur)}`\n" if fiat_disp_cur else "" f"∙ `{fmt_coin(profit_all_fiat, fiat_disp_cur)}`\n" if fiat_disp_cur else ""
) )
markdown_msg += ( markdown_msg += (
f"*ROI:* All trades\n" f"{all_roi_label}\n"
f"∙ `{fmt_coin(profit_all_coin, stake_cur)} " f"∙ `{fmt_coin(profit_all_coin, stake_cur)} "
f"({profit_all_ratio_mean:.2%}) " f"({profit_all_ratio_mean:.2%}) "
f"({profit_all_percent} \N{GREEK CAPITAL LETTER SIGMA}%)`\n" f"({profit_all_percent} \N{GREEK CAPITAL LETTER SIGMA}%)`\n"
@@ -1074,6 +1082,7 @@ class Telegram(RPCHandler):
f"*Winrate:* `{winrate:.2%}`\n" f"*Winrate:* `{winrate:.2%}`\n"
f"*Expectancy (Ratio):* `{expectancy:.2f} ({expectancy_ratio:.2f})`" f"*Expectancy (Ratio):* `{expectancy:.2f} ({expectancy_ratio:.2f})`"
) )
if stats["closed_trade_count"] > 0: if stats["closed_trade_count"] > 0:
markdown_msg += ( markdown_msg += (
f"\n*Avg. Duration:* `{avg_duration}`\n" f"\n*Avg. Duration:* `{avg_duration}`\n"
@@ -1092,21 +1101,27 @@ class Telegram(RPCHandler):
f" from `{stats['current_drawdown_start']} " f" from `{stats['current_drawdown_start']} "
f"({fmt_coin(stats['current_drawdown_high'], stake_cur)})`\n" f"({fmt_coin(stats['current_drawdown_high'], stake_cur)})`\n"
) )
await self._send_msg(
markdown_msg,
reload_able=True,
callback_path="update_profit",
query=update.callback_query,
)
@authorized_only return markdown_msg
async def _profit_long(self, update: Update, context: CallbackContext) -> None:
async def _profit_handler(
self,
update: Update,
context: CallbackContext,
direction: str | None = None,
callback_path: str = "update_profit",
) -> None:
""" """
Handler for /profit_long. Common handler for profit commands.
Returns cumulative profit statistics for long trades.
: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"] stake_cur = self._config["stake_currency"]
fiat_disp_cur = self._config.get("fiat_display_currency", "") fiat_disp_cur = self._config.get("fiat_display_currency", "")
start_date = datetime.fromtimestamp(0) start_date = datetime.fromtimestamp(0)
timescale = None timescale = None
try: try:
@@ -1116,189 +1131,57 @@ class Telegram(RPCHandler):
start_date = today_start - timedelta(days=timescale) start_date = today_start - timedelta(days=timescale)
except (TypeError, ValueError, IndexError): except (TypeError, ValueError, IndexError):
pass pass
stats = self._rpc._rpc_trade_statistics(
stake_cur, # Get stats with optional direction filter
fiat_disp_cur, stats_kwargs = {
start_date, "stake_currency": stake_cur,
direction="long" "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
) )
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( await self._send_msg(
markdown_msg, markdown_msg,
reload_able=True, reload_able=True,
callback_path="update_profit_long", callback_path=callback_path,
query=update.callback_query, 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", callback_path="update_profit_long"
)
@authorized_only @authorized_only
async def _profit_short(self, update: Update, context: CallbackContext) -> None: async def _profit_short(self, update: Update, context: CallbackContext) -> None:
""" """
Handler for /profit_short. Handler for /profit_short.
Returns cumulative profit statistics for short trades. Returns cumulative profit statistics for short trades.
""" """
stake_cur = self._config["stake_currency"] await self._profit_handler(
fiat_disp_cur = self._config.get("fiat_display_currency", "") update, context, direction="short", callback_path="update_profit_short"
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 @authorized_only
@@ -2383,4 +2266,3 @@ class Telegram(RPCHandler):
) )
except TelegramError as telegram_err: except TelegramError as telegram_err:
logger.warning("TelegramError: %s! Giving up on that message.", telegram_err.message) logger.warning("TelegramError: %s! Giving up on that message.", telegram_err.message)