mirror of
https://github.com/freqtrade/freqtrade.git
synced 2025-11-29 08:33:07 +00:00
Merge pull request #10212 from freqtrade/refactor/max_drawdown
Refactor calculate_max_drawdown
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import logging
|
import logging
|
||||||
import math
|
import math
|
||||||
|
from dataclasses import dataclass
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Dict, Tuple
|
from typing import Dict, Tuple
|
||||||
|
|
||||||
@@ -160,6 +161,16 @@ def calculate_underwater(
|
|||||||
return max_drawdown_df
|
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(
|
def calculate_max_drawdown(
|
||||||
trades: pd.DataFrame,
|
trades: pd.DataFrame,
|
||||||
*,
|
*,
|
||||||
@@ -167,14 +178,14 @@ def calculate_max_drawdown(
|
|||||||
value_col: str = "profit_abs",
|
value_col: str = "profit_abs",
|
||||||
starting_balance: float = 0,
|
starting_balance: float = 0,
|
||||||
relative: bool = False,
|
relative: bool = False,
|
||||||
) -> Tuple[float, pd.Timestamp, pd.Timestamp, float, float, float]:
|
) -> DrawDownResult:
|
||||||
"""
|
"""
|
||||||
Calculate max drawdown and the corresponding close dates
|
Calculate max drawdown and the corresponding close dates
|
||||||
:param trades: DataFrame containing trades (requires columns close_date and profit_ratio)
|
: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 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 value_col: Column in DataFrame to use for values (defaults to 'profit_abs')
|
||||||
:param starting_balance: Portfolio starting balance - properly calculate relative drawdown.
|
: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,
|
with absolute max drawdown, high and low time and high and low value,
|
||||||
and the relative account drawdown
|
and the relative account drawdown
|
||||||
:raise: ValueError if trade-dataframe was found empty.
|
:raise: ValueError if trade-dataframe was found empty.
|
||||||
@@ -201,13 +212,13 @@ def calculate_max_drawdown(
|
|||||||
low_val = max_drawdown_df.loc[idxmin, "cumulative"]
|
low_val = max_drawdown_df.loc[idxmin, "cumulative"]
|
||||||
max_drawdown_rel = max_drawdown_df.loc[idxmin, "drawdown_relative"]
|
max_drawdown_rel = max_drawdown_df.loc[idxmin, "drawdown_relative"]
|
||||||
|
|
||||||
return (
|
return DrawDownResult(
|
||||||
abs(max_drawdown_df.loc[idxmin, "drawdown"]),
|
drawdown_abs=abs(max_drawdown_df.loc[idxmin, "drawdown"]),
|
||||||
high_date,
|
high_date=high_date,
|
||||||
low_date,
|
low_date=low_date,
|
||||||
high_val,
|
high_value=high_val,
|
||||||
low_val,
|
low_value=low_val,
|
||||||
max_drawdown_rel,
|
relative_account_drawdown=max_drawdown_rel,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -350,9 +361,10 @@ def calculate_calmar(
|
|||||||
|
|
||||||
# calculate max drawdown
|
# calculate max drawdown
|
||||||
try:
|
try:
|
||||||
_, _, _, _, _, max_drawdown = calculate_max_drawdown(
|
drawdown = calculate_max_drawdown(
|
||||||
trades, value_col="profit_abs", starting_balance=starting_balance
|
trades, value_col="profit_abs", starting_balance=starting_balance
|
||||||
)
|
)
|
||||||
|
max_drawdown = drawdown.relative_account_drawdown
|
||||||
except ValueError:
|
except ValueError:
|
||||||
max_drawdown = 0
|
max_drawdown = 0
|
||||||
|
|
||||||
|
|||||||
@@ -42,4 +42,4 @@ class MaxDrawDownHyperOptLoss(IHyperOptLoss):
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
# No losing trade, therefore no drawdown.
|
# No losing trade, therefore no drawdown.
|
||||||
return -total_profit
|
return -total_profit
|
||||||
return -total_profit / max_drawdown[0]
|
return -total_profit / max_drawdown.drawdown_abs
|
||||||
|
|||||||
@@ -24,8 +24,9 @@ class ProfitDrawDownHyperOptLoss(IHyperOptLoss):
|
|||||||
total_profit = results["profit_abs"].sum()
|
total_profit = results["profit_abs"].sum()
|
||||||
|
|
||||||
try:
|
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:
|
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))
|
||||||
|
|||||||
@@ -497,29 +497,27 @@ def generate_strategy_stats(
|
|||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
max_drawdown_legacy, _, _, _, _, _ = calculate_max_drawdown(
|
max_drawdown_legacy = calculate_max_drawdown(results, value_col="profit_ratio")
|
||||||
results, value_col="profit_ratio"
|
drawdown = calculate_max_drawdown(
|
||||||
)
|
results, value_col="profit_abs", starting_balance=start_balance
|
||||||
(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_relative_drawdown = Underwater
|
# 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
|
results, value_col="profit_abs", starting_balance=start_balance, relative=True
|
||||||
)
|
)
|
||||||
|
|
||||||
strat_stats.update(
|
strat_stats.update(
|
||||||
{
|
{
|
||||||
"max_drawdown": max_drawdown_legacy, # Deprecated - do not use
|
"max_drawdown": max_drawdown_legacy.drawdown_abs, # Deprecated - do not use
|
||||||
"max_drawdown_account": max_drawdown,
|
"max_drawdown_account": drawdown.relative_account_drawdown,
|
||||||
"max_relative_drawdown": max_relative_drawdown,
|
"max_relative_drawdown": underwater.relative_account_drawdown,
|
||||||
"max_drawdown_abs": drawdown_abs,
|
"max_drawdown_abs": drawdown.drawdown_abs,
|
||||||
"drawdown_start": drawdown_start.strftime(DATETIME_PRINT_FORMAT),
|
"drawdown_start": drawdown.high_date.strftime(DATETIME_PRINT_FORMAT),
|
||||||
"drawdown_start_ts": drawdown_start.timestamp() * 1000,
|
"drawdown_start_ts": drawdown.high_date.timestamp() * 1000,
|
||||||
"drawdown_end": drawdown_end.strftime(DATETIME_PRINT_FORMAT),
|
"drawdown_end": drawdown.low_date.strftime(DATETIME_PRINT_FORMAT),
|
||||||
"drawdown_end_ts": drawdown_end.timestamp() * 1000,
|
"drawdown_end_ts": drawdown.low_date.timestamp() * 1000,
|
||||||
"max_drawdown_low": low_val,
|
"max_drawdown_low": drawdown.low_value,
|
||||||
"max_drawdown_high": high_val,
|
"max_drawdown_high": drawdown.high_value,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -179,19 +179,17 @@ def add_max_drawdown(
|
|||||||
Add scatter points indicating max drawdown
|
Add scatter points indicating max drawdown
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
_, highdate, lowdate, _, _, max_drawdown = calculate_max_drawdown(
|
drawdown = calculate_max_drawdown(trades, starting_balance=starting_balance)
|
||||||
trades, starting_balance=starting_balance
|
|
||||||
)
|
|
||||||
|
|
||||||
drawdown = go.Scatter(
|
drawdown = go.Scatter(
|
||||||
x=[highdate, lowdate],
|
x=[drawdown.high_date, drawdown.low_date],
|
||||||
y=[
|
y=[
|
||||||
df_comb.loc[timeframe_to_prev_date(timeframe, highdate), "cum_profit"],
|
df_comb.loc[timeframe_to_prev_date(timeframe, drawdown.high_date), "cum_profit"],
|
||||||
df_comb.loc[timeframe_to_prev_date(timeframe, lowdate), "cum_profit"],
|
df_comb.loc[timeframe_to_prev_date(timeframe, drawdown.low_date), "cum_profit"],
|
||||||
],
|
],
|
||||||
mode="markers",
|
mode="markers",
|
||||||
name=f"Max drawdown {max_drawdown:.2%}",
|
name=f"Max drawdown {drawdown.relative_account_drawdown:.2%}",
|
||||||
text=f"Max drawdown {max_drawdown:.2%}",
|
text=f"Max drawdown {drawdown.relative_account_drawdown:.2%}",
|
||||||
marker=dict(symbol="square-open", size=9, line=dict(width=2), color="green"),
|
marker=dict(symbol="square-open", size=9, line=dict(width=2), color="green"),
|
||||||
)
|
)
|
||||||
fig.add_trace(drawdown, row, 1)
|
fig.add_trace(drawdown, row, 1)
|
||||||
|
|||||||
@@ -59,7 +59,8 @@ class MaxDrawdown(IProtection):
|
|||||||
# Drawdown is always positive
|
# Drawdown is always positive
|
||||||
try:
|
try:
|
||||||
# TODO: This should use absolute profit calculation, considering account balance.
|
# 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:
|
except ValueError:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ from freqtrade import __version__
|
|||||||
from freqtrade.configuration.timerange import TimeRange
|
from freqtrade.configuration.timerange import TimeRange
|
||||||
from freqtrade.constants import CANCEL_REASON, DEFAULT_DATAFRAME_COLUMNS, Config
|
from freqtrade.constants import CANCEL_REASON, DEFAULT_DATAFRAME_COLUMNS, Config
|
||||||
from freqtrade.data.history import load_data
|
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 (
|
from freqtrade.enums import (
|
||||||
CandleType,
|
CandleType,
|
||||||
ExitCheckTuple,
|
ExitCheckTuple,
|
||||||
@@ -592,21 +592,10 @@ class RPC:
|
|||||||
|
|
||||||
expectancy, expectancy_ratio = calculate_expectancy(trades_df)
|
expectancy, expectancy_ratio = calculate_expectancy(trades_df)
|
||||||
|
|
||||||
max_drawdown_abs = 0.0
|
drawdown = DrawDownResult()
|
||||||
max_drawdown = 0.0
|
|
||||||
drawdown_start: Optional[datetime] = None
|
|
||||||
drawdown_end: Optional[datetime] = None
|
|
||||||
dd_high_val = dd_low_val = 0.0
|
|
||||||
if len(trades_df) > 0:
|
if len(trades_df) > 0:
|
||||||
try:
|
try:
|
||||||
(
|
drawdown = calculate_max_drawdown(
|
||||||
max_drawdown_abs,
|
|
||||||
drawdown_start,
|
|
||||||
drawdown_end,
|
|
||||||
dd_high_val,
|
|
||||||
dd_low_val,
|
|
||||||
max_drawdown,
|
|
||||||
) = calculate_max_drawdown(
|
|
||||||
trades_df,
|
trades_df,
|
||||||
value_col="profit_abs",
|
value_col="profit_abs",
|
||||||
date_col="close_date_dt",
|
date_col="close_date_dt",
|
||||||
@@ -663,14 +652,14 @@ class RPC:
|
|||||||
"winrate": winrate,
|
"winrate": winrate,
|
||||||
"expectancy": expectancy,
|
"expectancy": expectancy,
|
||||||
"expectancy_ratio": expectancy_ratio,
|
"expectancy_ratio": expectancy_ratio,
|
||||||
"max_drawdown": max_drawdown,
|
"max_drawdown": drawdown.relative_account_drawdown,
|
||||||
"max_drawdown_abs": max_drawdown_abs,
|
"max_drawdown_abs": drawdown.drawdown_abs,
|
||||||
"max_drawdown_start": format_date(drawdown_start),
|
"max_drawdown_start": format_date(drawdown.high_date),
|
||||||
"max_drawdown_start_timestamp": dt_ts_def(drawdown_start),
|
"max_drawdown_start_timestamp": dt_ts_def(drawdown.high_date),
|
||||||
"max_drawdown_end": format_date(drawdown_end),
|
"max_drawdown_end": format_date(drawdown.low_date),
|
||||||
"max_drawdown_end_timestamp": dt_ts_def(drawdown_end),
|
"max_drawdown_end_timestamp": dt_ts_def(drawdown.low_date),
|
||||||
"drawdown_high": dd_high_val,
|
"drawdown_high": drawdown.high_value,
|
||||||
"drawdown_low": dd_low_val,
|
"drawdown_low": drawdown.low_value,
|
||||||
"trading_volume": trading_volume,
|
"trading_volume": trading_volume,
|
||||||
"bot_start_timestamp": dt_ts_def(bot_start, 0),
|
"bot_start_timestamp": dt_ts_def(bot_start, 0),
|
||||||
"bot_start_date": format_date(bot_start),
|
"bot_start_date": format_date(bot_start),
|
||||||
|
|||||||
@@ -343,17 +343,15 @@ def test_create_cum_profit1(testdatadir):
|
|||||||
def test_calculate_max_drawdown(testdatadir):
|
def test_calculate_max_drawdown(testdatadir):
|
||||||
filename = testdatadir / "backtest_results/backtest-result.json"
|
filename = testdatadir / "backtest_results/backtest-result.json"
|
||||||
bt_data = load_backtest_data(filename)
|
bt_data = load_backtest_data(filename)
|
||||||
_, hdate, lowdate, hval, lval, drawdown = calculate_max_drawdown(
|
drawdown = calculate_max_drawdown(bt_data, value_col="profit_abs")
|
||||||
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, float)
|
assert isinstance(drawdown.high_date, Timestamp)
|
||||||
assert pytest.approx(drawdown) == 0.29753914
|
assert isinstance(drawdown.low_date, Timestamp)
|
||||||
assert isinstance(hdate, Timestamp)
|
assert isinstance(drawdown.high_value, float)
|
||||||
assert isinstance(lowdate, Timestamp)
|
assert isinstance(drawdown.low_value, float)
|
||||||
assert isinstance(hval, float)
|
assert drawdown.high_date == Timestamp("2018-01-16 19:30:00", tz="UTC")
|
||||||
assert isinstance(lval, float)
|
assert drawdown.low_date == Timestamp("2018-01-16 22:25:00", tz="UTC")
|
||||||
assert hdate == Timestamp("2018-01-16 19:30:00", tz="UTC")
|
|
||||||
assert lowdate == Timestamp("2018-01-16 22:25:00", tz="UTC")
|
|
||||||
|
|
||||||
underwater = calculate_underwater(bt_data)
|
underwater = calculate_underwater(bt_data)
|
||||||
assert isinstance(underwater, DataFrame)
|
assert isinstance(underwater, DataFrame)
|
||||||
@@ -509,19 +507,17 @@ def test_calculate_max_drawdown2():
|
|||||||
# sort by profit and reset index
|
# sort by profit and reset index
|
||||||
df = df.sort_values("profit").reset_index(drop=True)
|
df = df.sort_values("profit").reset_index(drop=True)
|
||||||
df1 = df.copy()
|
df1 = df.copy()
|
||||||
drawdown, hdate, ldate, hval, lval, drawdown_rel = calculate_max_drawdown(
|
drawdown = calculate_max_drawdown(df, date_col="open_date", value_col="profit")
|
||||||
df, date_col="open_date", value_col="profit"
|
|
||||||
)
|
|
||||||
# Ensure df has not been altered.
|
# Ensure df has not been altered.
|
||||||
assert df.equals(df1)
|
assert df.equals(df1)
|
||||||
|
|
||||||
assert isinstance(drawdown, float)
|
assert isinstance(drawdown.drawdown_abs, float)
|
||||||
assert isinstance(drawdown_rel, float)
|
assert isinstance(drawdown.relative_account_drawdown, float)
|
||||||
# High must be before low
|
# High must be before low
|
||||||
assert hdate < ldate
|
assert drawdown.high_date < drawdown.low_date
|
||||||
# High value must be higher than low value
|
# High value must be higher than low value
|
||||||
assert hval > lval
|
assert drawdown.high_value > drawdown.low_value
|
||||||
assert drawdown == 0.091755
|
assert drawdown.drawdown_abs == 0.091755
|
||||||
|
|
||||||
df = DataFrame(zip(values[:5], dates[:5]), columns=["profit", "open_date"])
|
df = DataFrame(zip(values[:5], dates[:5]), columns=["profit", "open_date"])
|
||||||
with pytest.raises(ValueError, match="No losing trade, therefore no drawdown."):
|
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 = DataFrame(zip(values[:5], dates[:5]), columns=["profit", "open_date"])
|
||||||
df1.loc[:, "profit"] = df1["profit"] * -1
|
df1.loc[:, "profit"] = df1["profit"] * -1
|
||||||
# No winning trade ...
|
# No winning trade ...
|
||||||
drawdown, hdate, ldate, hval, lval, drawdown_rel = calculate_max_drawdown(
|
drawdown = calculate_max_drawdown(df1, date_col="open_date", value_col="profit")
|
||||||
df1, date_col="open_date", value_col="profit"
|
assert drawdown.drawdown_abs == 0.043965
|
||||||
)
|
|
||||||
assert drawdown == 0.043965
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@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
|
# sort by profit and reset index
|
||||||
df = df.sort_values("profit_abs").reset_index(drop=True)
|
df = df.sort_values("profit_abs").reset_index(drop=True)
|
||||||
df1 = df.copy()
|
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
|
df, date_col="open_date", starting_balance=1000, relative=relative
|
||||||
)
|
)
|
||||||
# Ensure df has not been altered.
|
# Ensure df has not been altered.
|
||||||
assert df.equals(df1)
|
assert df.equals(df1)
|
||||||
|
|
||||||
assert isinstance(drawdown, float)
|
assert isinstance(drawdown.drawdown_abs, float)
|
||||||
assert isinstance(drawdown_rel, float)
|
assert isinstance(drawdown.relative_account_drawdown, float)
|
||||||
assert hdate == init_date + timedelta(days=highd)
|
assert drawdown.high_date == init_date + timedelta(days=highd)
|
||||||
assert ldate == init_date + timedelta(days=lowdays)
|
assert drawdown.low_date == init_date + timedelta(days=lowdays)
|
||||||
|
|
||||||
# High must be before low
|
# High must be before low
|
||||||
assert hdate < ldate
|
assert drawdown.high_date < drawdown.low_date
|
||||||
# High value must be higher than low value
|
# High value must be higher than low value
|
||||||
assert hval > lval
|
assert drawdown.high_value > drawdown.low_value
|
||||||
assert drawdown == result
|
assert drawdown.drawdown_abs == result
|
||||||
assert pytest.approx(drawdown_rel) == result_rel
|
assert pytest.approx(drawdown.relative_account_drawdown) == result_rel
|
||||||
|
|||||||
Reference in New Issue
Block a user