diff --git a/freqtrade/data/metrics.py b/freqtrade/data/metrics.py index 0bee68326..2e4673fb5 100644 --- a/freqtrade/data/metrics.py +++ b/freqtrade/data/metrics.py @@ -1,5 +1,6 @@ import logging import math +from dataclasses import dataclass from datetime import datetime from typing import Dict, Tuple @@ -160,6 +161,16 @@ def calculate_underwater( return max_drawdown_df +@dataclass() +class DrawDownResult: + 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 + relative_account_drawdown: float = 0.0 + + def calculate_max_drawdown( trades: pd.DataFrame, *, @@ -167,14 +178,14 @@ def calculate_max_drawdown( value_col: str = "profit_abs", starting_balance: float = 0, relative: bool = False, -) -> Tuple[float, pd.Timestamp, pd.Timestamp, float, float, float]: +) -> DrawDownResult: """ Calculate max drawdown and the corresponding close dates :param trades: DataFrame containing trades (requires columns close_date and profit_ratio) :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. - :return: Tuple (float, highdate, lowdate, highvalue, lowvalue, relative_drawdown) + :return: DrawDownResult object with absolute max drawdown, high and low time and high and low value, and the relative account drawdown :raise: ValueError if trade-dataframe was found empty. @@ -201,13 +212,13 @@ def calculate_max_drawdown( low_val = max_drawdown_df.loc[idxmin, "cumulative"] max_drawdown_rel = max_drawdown_df.loc[idxmin, "drawdown_relative"] - return ( - abs(max_drawdown_df.loc[idxmin, "drawdown"]), - high_date, - low_date, - high_val, - low_val, - max_drawdown_rel, + 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, ) @@ -350,9 +361,10 @@ def calculate_calmar( # calculate max drawdown try: - _, _, _, _, _, max_drawdown = calculate_max_drawdown( + drawdown = calculate_max_drawdown( trades, value_col="profit_abs", starting_balance=starting_balance ) + max_drawdown = drawdown.relative_account_drawdown except ValueError: max_drawdown = 0 diff --git a/freqtrade/optimize/hyperopt_loss/hyperopt_loss_max_drawdown.py b/freqtrade/optimize/hyperopt_loss/hyperopt_loss_max_drawdown.py index 4d6a7efac..ce1b29cf5 100644 --- a/freqtrade/optimize/hyperopt_loss/hyperopt_loss_max_drawdown.py +++ b/freqtrade/optimize/hyperopt_loss/hyperopt_loss_max_drawdown.py @@ -42,4 +42,4 @@ class MaxDrawDownHyperOptLoss(IHyperOptLoss): except ValueError: # No losing trade, therefore no drawdown. return -total_profit - return -total_profit / max_drawdown[0] + return -total_profit / max_drawdown.drawdown_abs diff --git a/freqtrade/optimize/hyperopt_loss/hyperopt_loss_profit_drawdown.py b/freqtrade/optimize/hyperopt_loss/hyperopt_loss_profit_drawdown.py index 99d28de0d..ac3f82700 100644 --- a/freqtrade/optimize/hyperopt_loss/hyperopt_loss_profit_drawdown.py +++ b/freqtrade/optimize/hyperopt_loss/hyperopt_loss_profit_drawdown.py @@ -24,8 +24,9 @@ class ProfitDrawDownHyperOptLoss(IHyperOptLoss): total_profit = results["profit_abs"].sum() try: - max_drawdown_abs = calculate_max_drawdown(results, value_col="profit_abs")[5] + drawdown = calculate_max_drawdown(results, value_col="profit_abs") + relative_account_drawdown = drawdown.relative_account_drawdown except ValueError: - max_drawdown_abs = 0 + relative_account_drawdown = 0 - return -1 * (total_profit * (1 - max_drawdown_abs * DRAWDOWN_MULT)) + return -1 * (total_profit * (1 - relative_account_drawdown * DRAWDOWN_MULT)) diff --git a/freqtrade/optimize/optimize_reports/optimize_reports.py b/freqtrade/optimize/optimize_reports/optimize_reports.py index 1fcf7b271..d40f3e73b 100644 --- a/freqtrade/optimize/optimize_reports/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports/optimize_reports.py @@ -497,29 +497,27 @@ def generate_strategy_stats( } try: - max_drawdown_legacy, _, _, _, _, _ = calculate_max_drawdown( - results, value_col="profit_ratio" - ) - (drawdown_abs, drawdown_start, drawdown_end, high_val, low_val, max_drawdown) = ( - calculate_max_drawdown(results, value_col="profit_abs", starting_balance=start_balance) + max_drawdown_legacy = calculate_max_drawdown(results, value_col="profit_ratio") + drawdown = calculate_max_drawdown( + results, value_col="profit_abs", starting_balance=start_balance ) # max_relative_drawdown = Underwater - (_, _, _, _, _, max_relative_drawdown) = calculate_max_drawdown( + underwater = calculate_max_drawdown( results, value_col="profit_abs", starting_balance=start_balance, relative=True ) strat_stats.update( { - "max_drawdown": max_drawdown_legacy, # Deprecated - do not use - "max_drawdown_account": max_drawdown, - "max_relative_drawdown": max_relative_drawdown, - "max_drawdown_abs": drawdown_abs, - "drawdown_start": drawdown_start.strftime(DATETIME_PRINT_FORMAT), - "drawdown_start_ts": drawdown_start.timestamp() * 1000, - "drawdown_end": drawdown_end.strftime(DATETIME_PRINT_FORMAT), - "drawdown_end_ts": drawdown_end.timestamp() * 1000, - "max_drawdown_low": low_val, - "max_drawdown_high": high_val, + "max_drawdown": max_drawdown_legacy.drawdown_abs, # Deprecated - do not use + "max_drawdown_account": drawdown.relative_account_drawdown, + "max_relative_drawdown": underwater.relative_account_drawdown, + "max_drawdown_abs": drawdown.drawdown_abs, + "drawdown_start": drawdown.high_date.strftime(DATETIME_PRINT_FORMAT), + "drawdown_start_ts": drawdown.high_date.timestamp() * 1000, + "drawdown_end": drawdown.low_date.strftime(DATETIME_PRINT_FORMAT), + "drawdown_end_ts": drawdown.low_date.timestamp() * 1000, + "max_drawdown_low": drawdown.low_value, + "max_drawdown_high": drawdown.high_value, } ) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index acb3a1999..de0910732 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -179,19 +179,17 @@ def add_max_drawdown( Add scatter points indicating max drawdown """ try: - _, highdate, lowdate, _, _, max_drawdown = calculate_max_drawdown( - trades, starting_balance=starting_balance - ) + drawdown = calculate_max_drawdown(trades, starting_balance=starting_balance) drawdown = go.Scatter( - x=[highdate, lowdate], + x=[drawdown.high_date, drawdown.low_date], y=[ - df_comb.loc[timeframe_to_prev_date(timeframe, highdate), "cum_profit"], - df_comb.loc[timeframe_to_prev_date(timeframe, lowdate), "cum_profit"], + df_comb.loc[timeframe_to_prev_date(timeframe, drawdown.high_date), "cum_profit"], + df_comb.loc[timeframe_to_prev_date(timeframe, drawdown.low_date), "cum_profit"], ], mode="markers", - name=f"Max drawdown {max_drawdown:.2%}", - text=f"Max drawdown {max_drawdown:.2%}", + name=f"Max drawdown {drawdown.relative_account_drawdown:.2%}", + text=f"Max drawdown {drawdown.relative_account_drawdown:.2%}", marker=dict(symbol="square-open", size=9, line=dict(width=2), color="green"), ) fig.add_trace(drawdown, row, 1) diff --git a/freqtrade/plugins/protections/max_drawdown_protection.py b/freqtrade/plugins/protections/max_drawdown_protection.py index 3e252185f..a1ba166fa 100644 --- a/freqtrade/plugins/protections/max_drawdown_protection.py +++ b/freqtrade/plugins/protections/max_drawdown_protection.py @@ -59,7 +59,8 @@ class MaxDrawdown(IProtection): # Drawdown is always positive try: # TODO: This should use absolute profit calculation, considering account balance. - drawdown, _, _, _, _, _ = calculate_max_drawdown(trades_df, value_col="close_profit") + drawdown_obj = calculate_max_drawdown(trades_df, value_col="close_profit") + drawdown = drawdown_obj.drawdown_abs except ValueError: return None diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index bcc262194..fd75da917 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -19,7 +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 calculate_expectancy, calculate_max_drawdown +from freqtrade.data.metrics import DrawDownResult, calculate_expectancy, calculate_max_drawdown from freqtrade.enums import ( CandleType, ExitCheckTuple, @@ -592,21 +592,10 @@ class RPC: expectancy, expectancy_ratio = calculate_expectancy(trades_df) - max_drawdown_abs = 0.0 - max_drawdown = 0.0 - drawdown_start: Optional[datetime] = None - drawdown_end: Optional[datetime] = None - dd_high_val = dd_low_val = 0.0 + drawdown = DrawDownResult() if len(trades_df) > 0: try: - ( - max_drawdown_abs, - drawdown_start, - drawdown_end, - dd_high_val, - dd_low_val, - max_drawdown, - ) = calculate_max_drawdown( + drawdown = calculate_max_drawdown( trades_df, value_col="profit_abs", date_col="close_date_dt", @@ -663,14 +652,14 @@ class RPC: "winrate": winrate, "expectancy": expectancy, "expectancy_ratio": expectancy_ratio, - "max_drawdown": max_drawdown, - "max_drawdown_abs": max_drawdown_abs, - "max_drawdown_start": format_date(drawdown_start), - "max_drawdown_start_timestamp": dt_ts_def(drawdown_start), - "max_drawdown_end": format_date(drawdown_end), - "max_drawdown_end_timestamp": dt_ts_def(drawdown_end), - "drawdown_high": dd_high_val, - "drawdown_low": dd_low_val, + "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, "trading_volume": trading_volume, "bot_start_timestamp": dt_ts_def(bot_start, 0), "bot_start_date": format_date(bot_start), diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index be4c638a7..d5dc8fc1e 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -343,17 +343,15 @@ def test_create_cum_profit1(testdatadir): def test_calculate_max_drawdown(testdatadir): filename = testdatadir / "backtest_results/backtest-result.json" bt_data = load_backtest_data(filename) - _, hdate, lowdate, hval, lval, drawdown = calculate_max_drawdown( - bt_data, value_col="profit_abs" - ) - assert isinstance(drawdown, float) - assert pytest.approx(drawdown) == 0.29753914 - assert isinstance(hdate, Timestamp) - assert isinstance(lowdate, Timestamp) - assert isinstance(hval, float) - assert isinstance(lval, float) - assert hdate == Timestamp("2018-01-16 19:30:00", tz="UTC") - assert lowdate == Timestamp("2018-01-16 22:25:00", tz="UTC") + drawdown = calculate_max_drawdown(bt_data, value_col="profit_abs") + assert isinstance(drawdown.relative_account_drawdown, float) + assert pytest.approx(drawdown.relative_account_drawdown) == 0.29753914 + assert isinstance(drawdown.high_date, Timestamp) + assert isinstance(drawdown.low_date, Timestamp) + assert isinstance(drawdown.high_value, float) + assert isinstance(drawdown.low_value, float) + assert drawdown.high_date == Timestamp("2018-01-16 19:30:00", tz="UTC") + assert drawdown.low_date == Timestamp("2018-01-16 22:25:00", tz="UTC") underwater = calculate_underwater(bt_data) assert isinstance(underwater, DataFrame) @@ -509,19 +507,17 @@ def test_calculate_max_drawdown2(): # sort by profit and reset index df = df.sort_values("profit").reset_index(drop=True) df1 = df.copy() - drawdown, hdate, ldate, hval, lval, drawdown_rel = calculate_max_drawdown( - df, date_col="open_date", value_col="profit" - ) + drawdown = calculate_max_drawdown(df, date_col="open_date", value_col="profit") # Ensure df has not been altered. assert df.equals(df1) - assert isinstance(drawdown, float) - assert isinstance(drawdown_rel, float) + assert isinstance(drawdown.drawdown_abs, float) + assert isinstance(drawdown.relative_account_drawdown, float) # High must be before low - assert hdate < ldate + assert drawdown.high_date < drawdown.low_date # High value must be higher than low value - assert hval > lval - assert drawdown == 0.091755 + assert drawdown.high_value > drawdown.low_value + assert drawdown.drawdown_abs == 0.091755 df = DataFrame(zip(values[:5], dates[:5]), columns=["profit", "open_date"]) with pytest.raises(ValueError, match="No losing trade, therefore no drawdown."): @@ -530,10 +526,8 @@ def test_calculate_max_drawdown2(): df1 = DataFrame(zip(values[:5], dates[:5]), columns=["profit", "open_date"]) df1.loc[:, "profit"] = df1["profit"] * -1 # No winning trade ... - drawdown, hdate, ldate, hval, lval, drawdown_rel = calculate_max_drawdown( - df1, date_col="open_date", value_col="profit" - ) - assert drawdown == 0.043965 + drawdown = calculate_max_drawdown(df1, date_col="open_date", value_col="profit") + assert drawdown.drawdown_abs == 0.043965 @pytest.mark.parametrize( @@ -555,20 +549,20 @@ def test_calculate_max_drawdown_abs(profits, relative, highd, lowdays, result, r # sort by profit and reset index df = df.sort_values("profit_abs").reset_index(drop=True) df1 = df.copy() - drawdown, hdate, ldate, hval, lval, drawdown_rel = calculate_max_drawdown( + drawdown = calculate_max_drawdown( df, date_col="open_date", starting_balance=1000, relative=relative ) # Ensure df has not been altered. assert df.equals(df1) - assert isinstance(drawdown, float) - assert isinstance(drawdown_rel, float) - assert hdate == init_date + timedelta(days=highd) - assert ldate == init_date + timedelta(days=lowdays) + assert isinstance(drawdown.drawdown_abs, float) + assert isinstance(drawdown.relative_account_drawdown, float) + assert drawdown.high_date == init_date + timedelta(days=highd) + assert drawdown.low_date == init_date + timedelta(days=lowdays) # High must be before low - assert hdate < ldate + assert drawdown.high_date < drawdown.low_date # High value must be higher than low value - assert hval > lval - assert drawdown == result - assert pytest.approx(drawdown_rel) == result_rel + assert drawdown.high_value > drawdown.low_value + assert drawdown.drawdown_abs == result + assert pytest.approx(drawdown.relative_account_drawdown) == result_rel