From 2ebc5374f4c6508d1cfbf7bb4aed180de3af5667 Mon Sep 17 00:00:00 2001 From: mrpabloyeah Date: Sat, 21 Jun 2025 12:10:53 +0200 Subject: [PATCH 1/4] Add current drawdown in telegram profit command --- freqtrade/data/metrics.py | 43 +++++++++++++++++++++++++++++++++++++++ freqtrade/rpc/rpc.py | 40 +++++++++++++++++++++++++----------- freqtrade/rpc/telegram.py | 4 ++++ 3 files changed, 75 insertions(+), 12 deletions(-) diff --git a/freqtrade/data/metrics.py b/freqtrade/data/metrics.py index 6a1ad2766..d7b3e0162 100644 --- a/freqtrade/data/metrics.py +++ b/freqtrade/data/metrics.py @@ -231,6 +231,49 @@ def calculate_max_drawdown( ) +def calculate_current_drawdown(trades: pd.DataFrame, starting_balance: float): + """ + Calculates the current drawdown (loss from historical maximum) based on closed trades. + + :param trades: DataFrame containing trades (requires columns close_date_dt and profit_abs) + :param starting_balance: Initial account balance + :return: DrawDownResult object including: + - drawdown_abs: Drawdown in absolute terms + - relative_account_drawdown: Drawdown relative to max balance + - high_value: Maximum profit reached + - high_date: Date when the max profit was reached + """ + + if len(trades) == 0: + raise ValueError("Trade dataframe empty.") + + # Sort trades by close date + sorted_df = trades.sort_values("close_date_dt").reset_index(drop=True) + + # Calculate cumulative profit + cum_profit = sorted_df["profit_abs"].cumsum() + + # Find historical maximum profit and its date + max_profit_idx = cum_profit.idxmax() + max_profit = cum_profit.iloc[max_profit_idx] + max_date = sorted_df.iloc[max_profit_idx]["close_date_dt"] + + # Calculate current and max balance + current_balance = starting_balance + cum_profit.iloc[-1] + max_balance = starting_balance + max_profit + + # Calculate drawdown + drawdown_abs = max_balance - current_balance + drawdown_relative = drawdown_abs / max_balance + + return DrawDownResult( + drawdown_abs=drawdown_abs, + relative_account_drawdown=drawdown_relative, + high_value=max_profit, + high_date=max_date, + ) + + def calculate_csum(trades: pd.DataFrame, starting_balance: float = 0) -> tuple[float, float]: """ Calculate min/max cumsum of trades, to show if the wallet/stake amount ratio is sane diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 01f649ba3..afb03e630 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -19,7 +19,12 @@ from freqtrade import __version__ from freqtrade.configuration.timerange import TimeRange from freqtrade.constants import CANCEL_REASON, DEFAULT_DATAFRAME_COLUMNS, Config from freqtrade.data.history import load_data -from freqtrade.data.metrics import DrawDownResult, calculate_expectancy, calculate_max_drawdown +from freqtrade.data.metrics import ( + DrawDownResult, + calculate_current_drawdown, + calculate_expectancy, + calculate_max_drawdown, +) from freqtrade.enums import ( CandleType, ExitCheckTuple, @@ -612,17 +617,23 @@ class RPC: expectancy, expectancy_ratio = calculate_expectancy(trades_df) - drawdown = DrawDownResult() + max_drawdown = DrawDownResult() + current_drawdown = DrawDownResult() + if len(trades_df) > 0: try: - drawdown = calculate_max_drawdown( + max_drawdown = calculate_max_drawdown( trades_df, value_col="profit_abs", date_col="close_date_dt", starting_balance=starting_balance, ) except ValueError: - # ValueError if no losing trade. + pass + + try: + current_drawdown = calculate_current_drawdown(trades_df, starting_balance) + except ValueError: pass profit_all_fiat = ( @@ -673,14 +684,19 @@ class RPC: "winrate": winrate, "expectancy": expectancy, "expectancy_ratio": expectancy_ratio, - "max_drawdown": drawdown.relative_account_drawdown, - "max_drawdown_abs": drawdown.drawdown_abs, - "max_drawdown_start": format_date(drawdown.high_date), - "max_drawdown_start_timestamp": dt_ts_def(drawdown.high_date), - "max_drawdown_end": format_date(drawdown.low_date), - "max_drawdown_end_timestamp": dt_ts_def(drawdown.low_date), - "drawdown_high": drawdown.high_value, - "drawdown_low": drawdown.low_value, + "max_drawdown": max_drawdown.relative_account_drawdown, + "max_drawdown_abs": max_drawdown.drawdown_abs, + "max_drawdown_start": format_date(max_drawdown.high_date), + "max_drawdown_start_timestamp": dt_ts_def(max_drawdown.high_date), + "max_drawdown_end": format_date(max_drawdown.low_date), + "max_drawdown_end_timestamp": dt_ts_def(max_drawdown.low_date), + "drawdown_high": max_drawdown.high_value, + "drawdown_low": max_drawdown.low_value, + "current_drawdown": current_drawdown.relative_account_drawdown, + "current_drawdown_abs": current_drawdown.drawdown_abs, + "current_drawdown_high": current_drawdown.high_value, + "current_drawdown_start": format_date(current_drawdown.high_date), + "current_drawdown_start_timestamp": dt_ts_def(current_drawdown.high_date), "trading_volume": trading_volume, "bot_start_timestamp": dt_ts_def(bot_start, 0), "bot_start_date": format_date(bot_start), diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 7230a8681..3e90a0861 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -1085,6 +1085,10 @@ class Telegram(RPCHandler): 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, From e6dd932436f6bc98fcf4f0f245b34cb7bb1ced8a Mon Sep 17 00:00:00 2001 From: mrpabloyeah Date: Tue, 8 Jul 2025 13:00:48 +0200 Subject: [PATCH 2/4] Expand calculate_max_drawdown() to return the current drawdown data and use it instead of calculate_current_drawdown() --- freqtrade/data/metrics.py | 122 +++++++++++++++++--------------------- freqtrade/rpc/rpc.py | 24 ++------ 2 files changed, 60 insertions(+), 86 deletions(-) diff --git a/freqtrade/data/metrics.py b/freqtrade/data/metrics.py index d7b3e0162..c1c37358f 100644 --- a/freqtrade/data/metrics.py +++ b/freqtrade/data/metrics.py @@ -174,12 +174,18 @@ def calculate_underwater( @dataclass() class DrawDownResult: - drawdown_abs: float = 0.0 + # Max drawdown fields high_date: pd.Timestamp = None low_date: pd.Timestamp = None high_value: float = 0.0 low_value: float = 0.0 + drawdown_abs: float = 0.0 relative_account_drawdown: float = 0.0 + # Current drawdown fields + current_high_date: pd.Timestamp = None + current_high_value: float = 0.0 + current_drawdown_abs: float = 0.0 + current_relative_account_drawdown: float = 0.0 def calculate_max_drawdown( @@ -191,88 +197,68 @@ def calculate_max_drawdown( relative: bool = False, ) -> DrawDownResult: """ - Calculate max drawdown and the corresponding close dates - :param trades: DataFrame containing trades (requires columns close_date and profit_ratio) + Calculate max drawdown and current drawdown with corresponding dates + :param trades: DataFrame containing trades (requires columns close_date and profit_abs) :param date_col: Column in DataFrame to use for dates (defaults to 'close_date') :param value_col: Column in DataFrame to use for values (defaults to 'profit_abs') :param starting_balance: Portfolio starting balance - properly calculate relative drawdown. + :param relative: If True, use relative drawdown for max calculation instead of absolute :return: DrawDownResult object with absolute max drawdown, high and low time and high and low value, - and the relative account drawdown + relative account drawdown, and current drawdown information. :raise: ValueError if trade-dataframe was found empty. """ - if len(trades) == 0: - raise ValueError("Trade dataframe empty.") - profit_results = trades.sort_values(date_col).reset_index(drop=True) - max_drawdown_df = _calc_drawdown_series( - profit_results, date_col=date_col, value_col=value_col, starting_balance=starting_balance - ) - - idxmin = ( - max_drawdown_df["drawdown_relative"].idxmax() - if relative - else max_drawdown_df["drawdown"].idxmin() - ) - - high_idx = max_drawdown_df.iloc[: idxmin + 1]["high_value"].idxmax() - high_date = profit_results.loc[high_idx, date_col] - low_date = profit_results.loc[idxmin, date_col] - high_val = max_drawdown_df.loc[high_idx, "cumulative"] - low_val = max_drawdown_df.loc[idxmin, "cumulative"] - max_drawdown_rel = max_drawdown_df.loc[idxmin, "drawdown_relative"] - - return DrawDownResult( - drawdown_abs=abs(max_drawdown_df.loc[idxmin, "drawdown"]), - high_date=high_date, - low_date=low_date, - high_value=high_val, - low_value=low_val, - relative_account_drawdown=max_drawdown_rel, - ) - - -def calculate_current_drawdown(trades: pd.DataFrame, starting_balance: float): - """ - Calculates the current drawdown (loss from historical maximum) based on closed trades. - - :param trades: DataFrame containing trades (requires columns close_date_dt and profit_abs) - :param starting_balance: Initial account balance - :return: DrawDownResult object including: - - drawdown_abs: Drawdown in absolute terms - - relative_account_drawdown: Drawdown relative to max balance - - high_value: Maximum profit reached - - high_date: Date when the max profit was reached - """ if len(trades) == 0: raise ValueError("Trade dataframe empty.") # Sort trades by close date - sorted_df = trades.sort_values("close_date_dt").reset_index(drop=True) + profit_results = trades.sort_values(date_col).reset_index(drop=True) - # Calculate cumulative profit - cum_profit = sorted_df["profit_abs"].cumsum() - - # Find historical maximum profit and its date - max_profit_idx = cum_profit.idxmax() - max_profit = cum_profit.iloc[max_profit_idx] - max_date = sorted_df.iloc[max_profit_idx]["close_date_dt"] - - # Calculate current and max balance - current_balance = starting_balance + cum_profit.iloc[-1] - max_balance = starting_balance + max_profit - - # Calculate drawdown - drawdown_abs = max_balance - current_balance - drawdown_relative = drawdown_abs / max_balance - - return DrawDownResult( - drawdown_abs=drawdown_abs, - relative_account_drawdown=drawdown_relative, - high_value=max_profit, - high_date=max_date, + # Get drawdown data + max_drawdown_df = _calc_drawdown_series( + profit_results, date_col=date_col, value_col=value_col, starting_balance=starting_balance ) + # Calculate maximum drawdown + idxmin = ( + max_drawdown_df["drawdown_relative"].idxmax() + if relative + else max_drawdown_df["drawdown"].idxmin() + ) + high_idx = max_drawdown_df.iloc[: idxmin + 1]["high_value"].idxmax() + high_date = profit_results.loc[high_idx, date_col] + low_date = profit_results.loc[idxmin, date_col] + high_val = max_drawdown_df.loc[high_idx, "cumulative"] + low_val = max_drawdown_df.loc[idxmin, "cumulative"] + max_drawdown_abs = abs(max_drawdown_df.loc[idxmin, "drawdown"]) + max_drawdown_rel = max_drawdown_df.loc[idxmin, "drawdown_relative"] + + # Calculate current drawdown + current_high_idx = max_drawdown_df["high_value"].iloc[:-1].idxmax() + current_high_date = profit_results.loc[current_high_idx, date_col] + current_high_value = max_drawdown_df.iloc[-1]["high_value"] + current_cumulative = max_drawdown_df.iloc[-1]["cumulative"] + current_drawdown_abs = current_high_value - current_cumulative + current_drawdown_relative = max_drawdown_df.iloc[-1]["drawdown_relative"] + + result = DrawDownResult( + # Max drawdown + high_date=high_date, + low_date=low_date, + high_value=high_val, + low_value=low_val, + drawdown_abs=max_drawdown_abs, + relative_account_drawdown=max_drawdown_rel, + # Current drawdown + current_high_date=current_high_date, + current_high_value=current_high_value, + current_drawdown_abs=current_drawdown_abs, + current_relative_account_drawdown=current_drawdown_relative, + ) + + return result + def calculate_csum(trades: pd.DataFrame, starting_balance: float = 0) -> tuple[float, float]: """ diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index afb03e630..838f1368c 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -19,12 +19,7 @@ from freqtrade import __version__ from freqtrade.configuration.timerange import TimeRange from freqtrade.constants import CANCEL_REASON, DEFAULT_DATAFRAME_COLUMNS, Config from freqtrade.data.history import load_data -from freqtrade.data.metrics import ( - DrawDownResult, - calculate_current_drawdown, - calculate_expectancy, - calculate_max_drawdown, -) +from freqtrade.data.metrics import DrawDownResult, calculate_expectancy, calculate_max_drawdown from freqtrade.enums import ( CandleType, ExitCheckTuple, @@ -616,9 +611,7 @@ class RPC: ) expectancy, expectancy_ratio = calculate_expectancy(trades_df) - max_drawdown = DrawDownResult() - current_drawdown = DrawDownResult() if len(trades_df) > 0: try: @@ -631,11 +624,6 @@ class RPC: except ValueError: pass - try: - current_drawdown = calculate_current_drawdown(trades_df, starting_balance) - except ValueError: - pass - profit_all_fiat = ( self._fiat_converter.convert_amount( profit_all_coin_sum, stake_currency, fiat_display_currency @@ -692,11 +680,11 @@ class RPC: "max_drawdown_end_timestamp": dt_ts_def(max_drawdown.low_date), "drawdown_high": max_drawdown.high_value, "drawdown_low": max_drawdown.low_value, - "current_drawdown": current_drawdown.relative_account_drawdown, - "current_drawdown_abs": current_drawdown.drawdown_abs, - "current_drawdown_high": current_drawdown.high_value, - "current_drawdown_start": format_date(current_drawdown.high_date), - "current_drawdown_start_timestamp": dt_ts_def(current_drawdown.high_date), + "current_drawdown": max_drawdown.current_relative_account_drawdown, + "current_drawdown_abs": max_drawdown.current_drawdown_abs, + "current_drawdown_high": max_drawdown.current_high_value, + "current_drawdown_start": format_date(max_drawdown.current_high_date), + "current_drawdown_start_timestamp": dt_ts_def(max_drawdown.current_high_date), "trading_volume": trading_volume, "bot_start_timestamp": dt_ts_def(bot_start, 0), "bot_start_date": format_date(bot_start), From fe92df7842ecee12134bbdd266e48a33b6e944f3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 8 Jul 2025 19:56:09 +0200 Subject: [PATCH 3/4] chore: revert unnecessary edits --- freqtrade/data/metrics.py | 13 +++---------- freqtrade/rpc/rpc.py | 31 ++++++++++++++++--------------- 2 files changed, 19 insertions(+), 25 deletions(-) diff --git a/freqtrade/data/metrics.py b/freqtrade/data/metrics.py index c1c37358f..7e0b279f4 100644 --- a/freqtrade/data/metrics.py +++ b/freqtrade/data/metrics.py @@ -175,11 +175,11 @@ def calculate_underwater( @dataclass() class DrawDownResult: # Max drawdown fields + drawdown_abs: float = 0.0 high_date: pd.Timestamp = None low_date: pd.Timestamp = None high_value: float = 0.0 low_value: float = 0.0 - drawdown_abs: float = 0.0 relative_account_drawdown: float = 0.0 # Current drawdown fields current_high_date: pd.Timestamp = None @@ -208,14 +208,10 @@ def calculate_max_drawdown( relative account drawdown, and current drawdown information. :raise: ValueError if trade-dataframe was found empty. """ - if len(trades) == 0: raise ValueError("Trade dataframe empty.") - # Sort trades by close date profit_results = trades.sort_values(date_col).reset_index(drop=True) - - # Get drawdown data max_drawdown_df = _calc_drawdown_series( profit_results, date_col=date_col, value_col=value_col, starting_balance=starting_balance ) @@ -231,7 +227,6 @@ def calculate_max_drawdown( low_date = profit_results.loc[idxmin, date_col] high_val = max_drawdown_df.loc[high_idx, "cumulative"] low_val = max_drawdown_df.loc[idxmin, "cumulative"] - max_drawdown_abs = abs(max_drawdown_df.loc[idxmin, "drawdown"]) max_drawdown_rel = max_drawdown_df.loc[idxmin, "drawdown_relative"] # Calculate current drawdown @@ -242,13 +237,13 @@ def calculate_max_drawdown( current_drawdown_abs = current_high_value - current_cumulative current_drawdown_relative = max_drawdown_df.iloc[-1]["drawdown_relative"] - result = DrawDownResult( + return DrawDownResult( # Max drawdown + drawdown_abs=abs(max_drawdown_df.loc[idxmin, "drawdown"]), high_date=high_date, low_date=low_date, high_value=high_val, low_value=low_val, - drawdown_abs=max_drawdown_abs, relative_account_drawdown=max_drawdown_rel, # Current drawdown current_high_date=current_high_date, @@ -257,8 +252,6 @@ def calculate_max_drawdown( current_relative_account_drawdown=current_drawdown_relative, ) - return result - def calculate_csum(trades: pd.DataFrame, starting_balance: float = 0) -> tuple[float, float]: """ diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 838f1368c..8bfa29ed0 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -611,17 +611,18 @@ class RPC: ) expectancy, expectancy_ratio = calculate_expectancy(trades_df) - max_drawdown = DrawDownResult() + drawdown = DrawDownResult() if len(trades_df) > 0: try: - max_drawdown = calculate_max_drawdown( + drawdown = calculate_max_drawdown( trades_df, value_col="profit_abs", date_col="close_date_dt", starting_balance=starting_balance, ) except ValueError: + # ValueError if no losing trade. pass profit_all_fiat = ( @@ -672,19 +673,19 @@ class RPC: "winrate": winrate, "expectancy": expectancy, "expectancy_ratio": expectancy_ratio, - "max_drawdown": max_drawdown.relative_account_drawdown, - "max_drawdown_abs": max_drawdown.drawdown_abs, - "max_drawdown_start": format_date(max_drawdown.high_date), - "max_drawdown_start_timestamp": dt_ts_def(max_drawdown.high_date), - "max_drawdown_end": format_date(max_drawdown.low_date), - "max_drawdown_end_timestamp": dt_ts_def(max_drawdown.low_date), - "drawdown_high": max_drawdown.high_value, - "drawdown_low": max_drawdown.low_value, - "current_drawdown": max_drawdown.current_relative_account_drawdown, - "current_drawdown_abs": max_drawdown.current_drawdown_abs, - "current_drawdown_high": max_drawdown.current_high_value, - "current_drawdown_start": format_date(max_drawdown.current_high_date), - "current_drawdown_start_timestamp": dt_ts_def(max_drawdown.current_high_date), + "max_drawdown": drawdown.relative_account_drawdown, + "max_drawdown_abs": drawdown.drawdown_abs, + "max_drawdown_start": format_date(drawdown.high_date), + "max_drawdown_start_timestamp": dt_ts_def(drawdown.high_date), + "max_drawdown_end": format_date(drawdown.low_date), + "max_drawdown_end_timestamp": dt_ts_def(drawdown.low_date), + "drawdown_high": drawdown.high_value, + "drawdown_low": drawdown.low_value, + "current_drawdown": drawdown.current_relative_account_drawdown, + "current_drawdown_abs": drawdown.current_drawdown_abs, + "current_drawdown_high": drawdown.current_high_value, + "current_drawdown_start": format_date(drawdown.current_high_date), + "current_drawdown_start_timestamp": dt_ts_def(drawdown.current_high_date), "trading_volume": trading_volume, "bot_start_timestamp": dt_ts_def(bot_start, 0), "bot_start_date": format_date(bot_start), From 2ecadabd92cc703cea9936d07ca9c834300c0467 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 8 Jul 2025 20:09:17 +0200 Subject: [PATCH 4/4] chore: align API endpoints --- freqtrade/rpc/api_server/api_schemas.py | 5 +++++ tests/rpc/test_rpc_apiserver.py | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index bb5dfafa7..5f89b4a4f 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -163,6 +163,11 @@ class Profit(BaseModel): max_drawdown_start_timestamp: int max_drawdown_end: str max_drawdown_end_timestamp: int + current_drawdown: float + current_drawdown_abs: float + current_drawdown_high: float + current_drawdown_start: str + current_drawdown_start_timestamp: int trading_volume: float | None = None bot_start_timestamp: int bot_start_date: str diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 573130a4a..120b4865b 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -1332,6 +1332,11 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, is_short, expected) "max_drawdown_start_timestamp": ANY, "max_drawdown_end": ANY, "max_drawdown_end_timestamp": ANY, + "current_drawdown": ANY, + "current_drawdown_abs": ANY, + "current_drawdown_high": ANY, + "current_drawdown_start": ANY, + "current_drawdown_start_timestamp": ANY, "trading_volume": expected["trading_volume"], "bot_start_timestamp": 0, "bot_start_date": "",