From c8eb22dcbdbe541dd2f306b76f84d078501569eb Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 14 May 2024 19:28:33 +0200 Subject: [PATCH 1/8] Add typed max_drawdown function --- freqtrade/data/metrics.py | 74 ++++++++++++++++++++++++++++++++++----- 1 file changed, 66 insertions(+), 8 deletions(-) diff --git a/freqtrade/data/metrics.py b/freqtrade/data/metrics.py index 0bee68326..1f353aab7 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,14 +161,33 @@ def calculate_underwater( return max_drawdown_df -def calculate_max_drawdown( +@dataclass() +class DrawDownResult: + __slots__ = [ + "drawdown_abs", + "high_date", + "low_date", + "high_value", + "low_value", + "relative_account_drawdown", + ] + + drawdown_abs: float + high_date: datetime + low_date: datetime + high_value: float + low_value: float + relative_account_drawdown: float + + +def calc_max_drawdown( trades: pd.DataFrame, *, date_col: str = "close_date", 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) @@ -201,13 +221,51 @@ def calculate_max_drawdown( 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_max_drawdown( + trades: pd.DataFrame, + *, + date_col: str = "close_date", + value_col: str = "profit_abs", + starting_balance: float = 0, + relative: bool = False, +) -> Tuple[float, pd.Timestamp, pd.Timestamp, float, float, float]: + """ + Calculate max drawdown and the corresponding close dates + Deprecated, favor calc_max_drawdown instead! + :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) + 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. + """ + # TODO: add deprecation warning + res = calc_max_drawdown( + trades, + date_col=date_col, + value_col=value_col, + starting_balance=starting_balance, + relative=relative, + ) return ( - abs(max_drawdown_df.loc[idxmin, "drawdown"]), - high_date, - low_date, - high_val, - low_val, - max_drawdown_rel, + res.drawdown_abs, + res.high_date, + res.low_date, + res.high_value, + res.low_value, + res.relative_account_drawdown, ) From 0aa3ec28453494061641138ab7c659df432c96f7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 14 May 2024 19:28:48 +0200 Subject: [PATCH 2/8] Have hyperopt-loss function use calc_max_drawdown --- .../hyperopt_loss/hyperopt_loss_profit_drawdown.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/freqtrade/optimize/hyperopt_loss/hyperopt_loss_profit_drawdown.py b/freqtrade/optimize/hyperopt_loss/hyperopt_loss_profit_drawdown.py index 99d28de0d..886979382 100644 --- a/freqtrade/optimize/hyperopt_loss/hyperopt_loss_profit_drawdown.py +++ b/freqtrade/optimize/hyperopt_loss/hyperopt_loss_profit_drawdown.py @@ -10,7 +10,7 @@ individual needs. from pandas import DataFrame -from freqtrade.data.metrics import calculate_max_drawdown +from freqtrade.data.metrics import calc_max_drawdown from freqtrade.optimize.hyperopt import IHyperOptLoss @@ -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 = calc_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)) From 94786454b75a1bdc62e5d7a21f1b53383a85ccca Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 14 May 2024 19:37:41 +0200 Subject: [PATCH 3/8] Use calc_drawdown method throughout the bot --- freqtrade/data/metrics.py | 3 +- .../hyperopt_loss_max_drawdown.py | 6 +-- .../optimize_reports/optimize_reports.py | 32 ++++++++-------- freqtrade/plot/plotting.py | 16 ++++---- .../protections/max_drawdown_protection.py | 5 ++- freqtrade/rpc/rpc.py | 37 ++++++++----------- 6 files changed, 45 insertions(+), 54 deletions(-) diff --git a/freqtrade/data/metrics.py b/freqtrade/data/metrics.py index 1f353aab7..87a60daf0 100644 --- a/freqtrade/data/metrics.py +++ b/freqtrade/data/metrics.py @@ -408,9 +408,10 @@ def calculate_calmar( # calculate max drawdown try: - _, _, _, _, _, max_drawdown = calculate_max_drawdown( + drawdown = calc_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..1e065d8ad 100644 --- a/freqtrade/optimize/hyperopt_loss/hyperopt_loss_max_drawdown.py +++ b/freqtrade/optimize/hyperopt_loss/hyperopt_loss_max_drawdown.py @@ -9,7 +9,7 @@ from datetime import datetime from pandas import DataFrame -from freqtrade.data.metrics import calculate_max_drawdown +from freqtrade.data.metrics import calc_max_drawdown from freqtrade.optimize.hyperopt import IHyperOptLoss @@ -38,8 +38,8 @@ class MaxDrawDownHyperOptLoss(IHyperOptLoss): """ total_profit = results["profit_abs"].sum() try: - max_drawdown = calculate_max_drawdown(results, value_col="profit_abs") + max_drawdown = calc_max_drawdown(results, value_col="profit_abs") 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/optimize_reports/optimize_reports.py b/freqtrade/optimize/optimize_reports/optimize_reports.py index 1fcf7b271..4f818c21b 100644 --- a/freqtrade/optimize/optimize_reports/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports/optimize_reports.py @@ -8,12 +8,12 @@ from pandas import DataFrame, Series, concat, to_datetime from freqtrade.constants import BACKTEST_BREAKDOWNS, DATETIME_PRINT_FORMAT from freqtrade.data.metrics import ( + calc_max_drawdown, calculate_cagr, calculate_calmar, calculate_csum, calculate_expectancy, calculate_market_change, - calculate_max_drawdown, calculate_sharpe, calculate_sortino, ) @@ -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 = calc_max_drawdown(results, value_col="profit_ratio") + drawdown = calc_max_drawdown( + results, value_col="profit_abs", starting_balance=start_balance ) # max_relative_drawdown = Underwater - (_, _, _, _, _, max_relative_drawdown) = calculate_max_drawdown( + underwater = calc_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..0f0e9d527 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -16,7 +16,7 @@ from freqtrade.data.converter import trim_dataframe from freqtrade.data.dataprovider import DataProvider from freqtrade.data.history import get_timerange, load_data from freqtrade.data.metrics import ( - calculate_max_drawdown, + calc_max_drawdown, calculate_underwater, combine_dataframes_with_mean, create_cum_profit, @@ -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 = calc_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..852e2dca5 100644 --- a/freqtrade/plugins/protections/max_drawdown_protection.py +++ b/freqtrade/plugins/protections/max_drawdown_protection.py @@ -5,7 +5,7 @@ from typing import Any, Dict, Optional import pandas as pd from freqtrade.constants import Config, LongShort -from freqtrade.data.metrics import calculate_max_drawdown +from freqtrade.data.metrics import calc_max_drawdown from freqtrade.persistence import Trade from freqtrade.plugins.protections import IProtection, ProtectionReturn @@ -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 = calc_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..c949caea4 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -19,7 +19,11 @@ 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, + calc_max_drawdown, + calculate_expectancy, +) from freqtrade.enums import ( CandleType, ExitCheckTuple, @@ -592,21 +596,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(0.0, 0.0, None, None, 0.0) 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 = calc_max_drawdown( trades_df, value_col="profit_abs", date_col="close_date_dt", @@ -663,14 +656,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), From bcb59265b5a2b1f76f2617e16d019e25fc470428 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 14 May 2024 19:50:35 +0200 Subject: [PATCH 4/8] Use default parameters for DrawdownResult --- freqtrade/data/metrics.py | 21 ++++++--------------- freqtrade/rpc/rpc.py | 2 +- 2 files changed, 7 insertions(+), 16 deletions(-) diff --git a/freqtrade/data/metrics.py b/freqtrade/data/metrics.py index 87a60daf0..09410667b 100644 --- a/freqtrade/data/metrics.py +++ b/freqtrade/data/metrics.py @@ -163,21 +163,12 @@ def calculate_underwater( @dataclass() class DrawDownResult: - __slots__ = [ - "drawdown_abs", - "high_date", - "low_date", - "high_value", - "low_value", - "relative_account_drawdown", - ] - - drawdown_abs: float - high_date: datetime - low_date: datetime - high_value: float - low_value: float - relative_account_drawdown: float + drawdown_abs: float = 0.0 + high_date: datetime = None + low_date: datetime = None + high_value: float = 0.0 + low_value: float = 0.0 + relative_account_drawdown: float = 0.0 def calc_max_drawdown( diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index c949caea4..4f60e3049 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -596,7 +596,7 @@ class RPC: expectancy, expectancy_ratio = calculate_expectancy(trades_df) - drawdown = DrawDownResult(0.0, 0.0, None, None, 0.0) + drawdown = DrawDownResult() if len(trades_df) > 0: try: drawdown = calc_max_drawdown( From a6050cb7717cdcc4428ad5ebfe17d05822b452d7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 14 May 2024 19:57:46 +0200 Subject: [PATCH 5/8] Update tests for new interface --- tests/data/test_btanalysis.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index be4c638a7..9ad7418bc 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -20,6 +20,7 @@ from freqtrade.data.btanalysis import ( ) from freqtrade.data.history import load_data, load_pair_history from freqtrade.data.metrics import ( + calc_max_drawdown, calculate_cagr, calculate_calmar, calculate_csum, @@ -343,23 +344,21 @@ 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 = calc_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) with pytest.raises(ValueError, match="Trade dataframe empty."): - calculate_max_drawdown(DataFrame()) + calc_max_drawdown(DataFrame()) with pytest.raises(ValueError, match="Trade dataframe empty."): calculate_underwater(DataFrame()) From c79b75ff9a5d59acbb7d6ec44dc69a4998f12360 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 15 May 2024 06:46:30 +0200 Subject: [PATCH 6/8] Update remaining tests --- tests/data/test_btanalysis.py | 42 +++++++++++++++-------------------- 1 file changed, 18 insertions(+), 24 deletions(-) diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index 9ad7418bc..c78e85421 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -508,31 +508,27 @@ 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 = calc_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."): - calculate_max_drawdown(df, date_col="open_date", value_col="profit") + calc_max_drawdown(df, date_col="open_date", value_col="profit") 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 = calc_max_drawdown(df1, date_col="open_date", value_col="profit") + assert drawdown.drawdown_abs == 0.043965 @pytest.mark.parametrize( @@ -554,20 +550,18 @@ 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( - df, date_col="open_date", starting_balance=1000, relative=relative - ) + drawdown = calc_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 From a6b07ec96f5cfe2332fccba0677e6b0d0dc6eda3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 15 May 2024 06:54:17 +0200 Subject: [PATCH 7/8] Remove compatibility layer for calculate_max_drawdown --- freqtrade/data/metrics.py | 44 ++----------------- .../hyperopt_loss_max_drawdown.py | 4 +- .../hyperopt_loss_profit_drawdown.py | 4 +- .../optimize_reports/optimize_reports.py | 8 ++-- freqtrade/plot/plotting.py | 4 +- .../protections/max_drawdown_protection.py | 4 +- freqtrade/rpc/rpc.py | 4 +- tests/data/test_btanalysis.py | 15 ++++--- 8 files changed, 25 insertions(+), 62 deletions(-) diff --git a/freqtrade/data/metrics.py b/freqtrade/data/metrics.py index 09410667b..83c30bb68 100644 --- a/freqtrade/data/metrics.py +++ b/freqtrade/data/metrics.py @@ -171,7 +171,7 @@ class DrawDownResult: relative_account_drawdown: float = 0.0 -def calc_max_drawdown( +def calculate_max_drawdown( trades: pd.DataFrame, *, date_col: str = "close_date", @@ -185,7 +185,7 @@ def calc_max_drawdown( :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. @@ -222,44 +222,6 @@ def calc_max_drawdown( ) -def calculate_max_drawdown( - trades: pd.DataFrame, - *, - date_col: str = "close_date", - value_col: str = "profit_abs", - starting_balance: float = 0, - relative: bool = False, -) -> Tuple[float, pd.Timestamp, pd.Timestamp, float, float, float]: - """ - Calculate max drawdown and the corresponding close dates - Deprecated, favor calc_max_drawdown instead! - :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) - 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. - """ - # TODO: add deprecation warning - res = calc_max_drawdown( - trades, - date_col=date_col, - value_col=value_col, - starting_balance=starting_balance, - relative=relative, - ) - return ( - res.drawdown_abs, - res.high_date, - res.low_date, - res.high_value, - res.low_value, - res.relative_account_drawdown, - ) - - 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 @@ -399,7 +361,7 @@ def calculate_calmar( # calculate max drawdown try: - drawdown = calc_max_drawdown( + drawdown = calculate_max_drawdown( trades, value_col="profit_abs", starting_balance=starting_balance ) max_drawdown = drawdown.relative_account_drawdown diff --git a/freqtrade/optimize/hyperopt_loss/hyperopt_loss_max_drawdown.py b/freqtrade/optimize/hyperopt_loss/hyperopt_loss_max_drawdown.py index 1e065d8ad..ce1b29cf5 100644 --- a/freqtrade/optimize/hyperopt_loss/hyperopt_loss_max_drawdown.py +++ b/freqtrade/optimize/hyperopt_loss/hyperopt_loss_max_drawdown.py @@ -9,7 +9,7 @@ from datetime import datetime from pandas import DataFrame -from freqtrade.data.metrics import calc_max_drawdown +from freqtrade.data.metrics import calculate_max_drawdown from freqtrade.optimize.hyperopt import IHyperOptLoss @@ -38,7 +38,7 @@ class MaxDrawDownHyperOptLoss(IHyperOptLoss): """ total_profit = results["profit_abs"].sum() try: - max_drawdown = calc_max_drawdown(results, value_col="profit_abs") + max_drawdown = calculate_max_drawdown(results, value_col="profit_abs") except ValueError: # No losing trade, therefore no drawdown. return -total_profit diff --git a/freqtrade/optimize/hyperopt_loss/hyperopt_loss_profit_drawdown.py b/freqtrade/optimize/hyperopt_loss/hyperopt_loss_profit_drawdown.py index 886979382..ac3f82700 100644 --- a/freqtrade/optimize/hyperopt_loss/hyperopt_loss_profit_drawdown.py +++ b/freqtrade/optimize/hyperopt_loss/hyperopt_loss_profit_drawdown.py @@ -10,7 +10,7 @@ individual needs. from pandas import DataFrame -from freqtrade.data.metrics import calc_max_drawdown +from freqtrade.data.metrics import calculate_max_drawdown from freqtrade.optimize.hyperopt import IHyperOptLoss @@ -24,7 +24,7 @@ class ProfitDrawDownHyperOptLoss(IHyperOptLoss): total_profit = results["profit_abs"].sum() try: - drawdown = calc_max_drawdown(results, value_col="profit_abs") + drawdown = calculate_max_drawdown(results, value_col="profit_abs") relative_account_drawdown = drawdown.relative_account_drawdown except ValueError: relative_account_drawdown = 0 diff --git a/freqtrade/optimize/optimize_reports/optimize_reports.py b/freqtrade/optimize/optimize_reports/optimize_reports.py index 4f818c21b..d40f3e73b 100644 --- a/freqtrade/optimize/optimize_reports/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports/optimize_reports.py @@ -8,12 +8,12 @@ from pandas import DataFrame, Series, concat, to_datetime from freqtrade.constants import BACKTEST_BREAKDOWNS, DATETIME_PRINT_FORMAT from freqtrade.data.metrics import ( - calc_max_drawdown, calculate_cagr, calculate_calmar, calculate_csum, calculate_expectancy, calculate_market_change, + calculate_max_drawdown, calculate_sharpe, calculate_sortino, ) @@ -497,12 +497,12 @@ def generate_strategy_stats( } try: - max_drawdown_legacy = calc_max_drawdown(results, value_col="profit_ratio") - drawdown = calc_max_drawdown( + 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 - underwater = calc_max_drawdown( + underwater = calculate_max_drawdown( results, value_col="profit_abs", starting_balance=start_balance, relative=True ) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 0f0e9d527..de0910732 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -16,7 +16,7 @@ from freqtrade.data.converter import trim_dataframe from freqtrade.data.dataprovider import DataProvider from freqtrade.data.history import get_timerange, load_data from freqtrade.data.metrics import ( - calc_max_drawdown, + calculate_max_drawdown, calculate_underwater, combine_dataframes_with_mean, create_cum_profit, @@ -179,7 +179,7 @@ def add_max_drawdown( Add scatter points indicating max drawdown """ try: - drawdown = calc_max_drawdown(trades, starting_balance=starting_balance) + drawdown = calculate_max_drawdown(trades, starting_balance=starting_balance) drawdown = go.Scatter( x=[drawdown.high_date, drawdown.low_date], diff --git a/freqtrade/plugins/protections/max_drawdown_protection.py b/freqtrade/plugins/protections/max_drawdown_protection.py index 852e2dca5..a1ba166fa 100644 --- a/freqtrade/plugins/protections/max_drawdown_protection.py +++ b/freqtrade/plugins/protections/max_drawdown_protection.py @@ -5,7 +5,7 @@ from typing import Any, Dict, Optional import pandas as pd from freqtrade.constants import Config, LongShort -from freqtrade.data.metrics import calc_max_drawdown +from freqtrade.data.metrics import calculate_max_drawdown from freqtrade.persistence import Trade from freqtrade.plugins.protections import IProtection, ProtectionReturn @@ -59,7 +59,7 @@ class MaxDrawdown(IProtection): # Drawdown is always positive try: # TODO: This should use absolute profit calculation, considering account balance. - drawdown_obj = calc_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 4f60e3049..06c6c571b 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -21,8 +21,8 @@ from freqtrade.constants import CANCEL_REASON, DEFAULT_DATAFRAME_COLUMNS, Config from freqtrade.data.history import load_data from freqtrade.data.metrics import ( DrawDownResult, - calc_max_drawdown, calculate_expectancy, + calculate_max_drawdown, ) from freqtrade.enums import ( CandleType, @@ -599,7 +599,7 @@ class RPC: drawdown = DrawDownResult() if len(trades_df) > 0: try: - drawdown = calc_max_drawdown( + drawdown = calculate_max_drawdown( trades_df, value_col="profit_abs", date_col="close_date_dt", diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index c78e85421..d5dc8fc1e 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -20,7 +20,6 @@ from freqtrade.data.btanalysis import ( ) from freqtrade.data.history import load_data, load_pair_history from freqtrade.data.metrics import ( - calc_max_drawdown, calculate_cagr, calculate_calmar, calculate_csum, @@ -344,7 +343,7 @@ 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) - drawdown = calc_max_drawdown(bt_data, value_col="profit_abs") + 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) @@ -358,7 +357,7 @@ def test_calculate_max_drawdown(testdatadir): assert isinstance(underwater, DataFrame) with pytest.raises(ValueError, match="Trade dataframe empty."): - calc_max_drawdown(DataFrame()) + calculate_max_drawdown(DataFrame()) with pytest.raises(ValueError, match="Trade dataframe empty."): calculate_underwater(DataFrame()) @@ -508,7 +507,7 @@ def test_calculate_max_drawdown2(): # sort by profit and reset index df = df.sort_values("profit").reset_index(drop=True) df1 = df.copy() - drawdown = calc_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) @@ -522,12 +521,12 @@ def test_calculate_max_drawdown2(): df = DataFrame(zip(values[:5], dates[:5]), columns=["profit", "open_date"]) with pytest.raises(ValueError, match="No losing trade, therefore no drawdown."): - calc_max_drawdown(df, date_col="open_date", value_col="profit") + calculate_max_drawdown(df, date_col="open_date", value_col="profit") df1 = DataFrame(zip(values[:5], dates[:5]), columns=["profit", "open_date"]) df1.loc[:, "profit"] = df1["profit"] * -1 # No winning trade ... - drawdown = calc_max_drawdown(df1, date_col="open_date", value_col="profit") + drawdown = calculate_max_drawdown(df1, date_col="open_date", value_col="profit") assert drawdown.drawdown_abs == 0.043965 @@ -550,7 +549,9 @@ 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 = calc_max_drawdown(df, date_col="open_date", starting_balance=1000, relative=relative) + drawdown = calculate_max_drawdown( + df, date_col="open_date", starting_balance=1000, relative=relative + ) # Ensure df has not been altered. assert df.equals(df1) From 702ac14f2769d401ed9b7b93f6bec2ac61df6da5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 15 May 2024 06:57:28 +0200 Subject: [PATCH 8/8] Fix using wrong type --- freqtrade/data/metrics.py | 4 ++-- freqtrade/rpc/rpc.py | 6 +----- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/freqtrade/data/metrics.py b/freqtrade/data/metrics.py index 83c30bb68..2e4673fb5 100644 --- a/freqtrade/data/metrics.py +++ b/freqtrade/data/metrics.py @@ -164,8 +164,8 @@ def calculate_underwater( @dataclass() class DrawDownResult: drawdown_abs: float = 0.0 - high_date: datetime = None - low_date: datetime = None + 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 diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 06c6c571b..fd75da917 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -19,11 +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_expectancy, - calculate_max_drawdown, -) +from freqtrade.data.metrics import DrawDownResult, calculate_expectancy, calculate_max_drawdown from freqtrade.enums import ( CandleType, ExitCheckTuple,