Use absolute drawdown calc

This commit is contained in:
Matthias 2021-02-14 19:30:17 +01:00
parent 74fc4bdab5
commit 0d2f877e77
5 changed files with 38 additions and 14 deletions

View File

@ -360,13 +360,14 @@ def create_cum_profit(df: pd.DataFrame, trades: pd.DataFrame, col_name: str,
def calculate_max_drawdown(trades: pd.DataFrame, *, date_col: str = 'close_date', def calculate_max_drawdown(trades: pd.DataFrame, *, date_col: str = 'close_date',
value_col: str = 'profit_ratio' value_col: str = 'profit_ratio'
) -> Tuple[float, pd.Timestamp, pd.Timestamp]: ) -> Tuple[float, pd.Timestamp, pd.Timestamp, float, float]:
""" """
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_ratio') :param value_col: Column in DataFrame to use for values (defaults to 'profit_ratio')
:return: Tuple (float, highdate, lowdate) with absolute max drawdown, high and low time :return: Tuple (float, highdate, lowdate, highvalue, lowvalue) with absolute max drawdown,
high and low time and high and low value.
:raise: ValueError if trade-dataframe was found empty. :raise: ValueError if trade-dataframe was found empty.
""" """
if len(trades) == 0: if len(trades) == 0:
@ -382,7 +383,10 @@ def calculate_max_drawdown(trades: pd.DataFrame, *, date_col: str = 'close_date'
raise ValueError("No losing trade, therefore no drawdown.") raise ValueError("No losing trade, therefore no drawdown.")
high_date = profit_results.loc[max_drawdown_df.iloc[:idxmin]['high_value'].idxmax(), date_col] high_date = profit_results.loc[max_drawdown_df.iloc[:idxmin]['high_value'].idxmax(), date_col]
low_date = profit_results.loc[idxmin, date_col] low_date = profit_results.loc[idxmin, date_col]
return abs(min(max_drawdown_df['drawdown'])), high_date, low_date high_val = max_drawdown_df.loc[max_drawdown_df.iloc[:idxmin]
['high_value'].idxmax(), 'cumulative']
low_val = max_drawdown_df.loc[idxmin, 'cumulative']
return abs(min(max_drawdown_df['drawdown'])), high_date, low_date, high_val, low_val
def calculate_csum(trades: pd.DataFrame) -> Tuple[float, float]: def calculate_csum(trades: pd.DataFrame) -> Tuple[float, float]:

View File

@ -322,14 +322,20 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame],
result['strategy'][strategy] = strat_stats result['strategy'][strategy] = strat_stats
try: try:
max_drawdown, drawdown_start, drawdown_end = calculate_max_drawdown( max_drawdown, _, _, _, _ = calculate_max_drawdown(
results, value_col='profit_ratio') results, value_col='profit_ratio')
drawdown_abs, drawdown_start, drawdown_end, high_val, low_val = calculate_max_drawdown(
results, value_col='profit_abs')
strat_stats.update({ strat_stats.update({
'max_drawdown': max_drawdown, 'max_drawdown': max_drawdown,
'max_drawdown_abs': drawdown_abs,
'drawdown_start': drawdown_start, 'drawdown_start': drawdown_start,
'drawdown_start_ts': drawdown_start.timestamp() * 1000, 'drawdown_start_ts': drawdown_start.timestamp() * 1000,
'drawdown_end': drawdown_end, 'drawdown_end': drawdown_end,
'drawdown_end_ts': drawdown_end.timestamp() * 1000, 'drawdown_end_ts': drawdown_end.timestamp() * 1000,
'max_drawdown_low': low_val,
'max_drawdown_high': high_val,
}) })
csum_min, csum_max = calculate_csum(results) csum_min, csum_max = calculate_csum(results)
@ -341,6 +347,9 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame],
except ValueError: except ValueError:
strat_stats.update({ strat_stats.update({
'max_drawdown': 0.0, 'max_drawdown': 0.0,
'max_drawdown_abs': 0.0,
'max_drawdown_low': 0.0,
'max_drawdown_high': 0.0,
'drawdown_start': datetime(1970, 1, 1, tzinfo=timezone.utc), 'drawdown_start': datetime(1970, 1, 1, tzinfo=timezone.utc),
'drawdown_start_ts': 0, 'drawdown_start_ts': 0,
'drawdown_end': datetime(1970, 1, 1, tzinfo=timezone.utc), 'drawdown_end': datetime(1970, 1, 1, tzinfo=timezone.utc),
@ -471,6 +480,12 @@ def text_table_add_metrics(strat_results: Dict) -> str:
strat_results['stake_currency'])), strat_results['stake_currency'])),
('Max Drawdown', f"{round(strat_results['max_drawdown'] * 100, 2)}%"), ('Max Drawdown', f"{round(strat_results['max_drawdown'] * 100, 2)}%"),
('Max Drawdown', round_coin_value(strat_results['max_drawdown_abs'],
strat_results['stake_currency'])),
('Max Drawdown high', round_coin_value(strat_results['max_drawdown_high'],
strat_results['stake_currency'])),
('Max Drawdown low', round_coin_value(strat_results['max_drawdown_low'],
strat_results['stake_currency'])),
('Drawdown Start', strat_results['drawdown_start'].strftime(DATETIME_PRINT_FORMAT)), ('Drawdown Start', strat_results['drawdown_start'].strftime(DATETIME_PRINT_FORMAT)),
('Drawdown End', strat_results['drawdown_end'].strftime(DATETIME_PRINT_FORMAT)), ('Drawdown End', strat_results['drawdown_end'].strftime(DATETIME_PRINT_FORMAT)),
('Market change', f"{round(strat_results['market_change'] * 100, 2)}%"), ('Market change', f"{round(strat_results['market_change'] * 100, 2)}%"),

View File

@ -145,7 +145,7 @@ def add_max_drawdown(fig, row, trades: pd.DataFrame, df_comb: pd.DataFrame,
Add scatter points indicating max drawdown Add scatter points indicating max drawdown
""" """
try: try:
max_drawdown, highdate, lowdate = calculate_max_drawdown(trades) max_drawdown, highdate, lowdate, _, _ = calculate_max_drawdown(trades)
drawdown = go.Scatter( drawdown = go.Scatter(
x=[highdate, lowdate], x=[highdate, lowdate],

View File

@ -55,7 +55,7 @@ class MaxDrawdown(IProtection):
# Drawdown is always positive # Drawdown is always positive
try: try:
drawdown, _, _ = calculate_max_drawdown(trades_df, value_col='close_profit') drawdown, _, _, _, _ = calculate_max_drawdown(trades_df, value_col='close_profit')
except ValueError: except ValueError:
return False, None, None return False, None, None

View File

@ -274,15 +274,17 @@ def test_create_cum_profit1(testdatadir):
def test_calculate_max_drawdown(testdatadir): def test_calculate_max_drawdown(testdatadir):
filename = testdatadir / "backtest-result_test.json" filename = testdatadir / "backtest-result_test.json"
bt_data = load_backtest_data(filename) bt_data = load_backtest_data(filename)
drawdown, h, low = calculate_max_drawdown(bt_data) drawdown, hdate, lowdate, hval, lval = calculate_max_drawdown(bt_data)
assert isinstance(drawdown, float) assert isinstance(drawdown, float)
assert pytest.approx(drawdown) == 0.21142322 assert pytest.approx(drawdown) == 0.21142322
assert isinstance(h, Timestamp) assert isinstance(hdate, Timestamp)
assert isinstance(low, Timestamp) assert isinstance(lowdate, Timestamp)
assert h == Timestamp('2018-01-24 14:25:00', tz='UTC') assert isinstance(hval, float)
assert low == Timestamp('2018-01-30 04:45:00', tz='UTC') assert isinstance(lval, float)
assert hdate == Timestamp('2018-01-24 14:25:00', tz='UTC')
assert lowdate == Timestamp('2018-01-30 04:45:00', tz='UTC')
with pytest.raises(ValueError, match='Trade dataframe empty.'): with pytest.raises(ValueError, match='Trade dataframe empty.'):
drawdown, h, low = calculate_max_drawdown(DataFrame()) drawdown, hdate, lowdate, hval, lval = calculate_max_drawdown(DataFrame())
def test_calculate_csum(testdatadir): def test_calculate_csum(testdatadir):
@ -310,13 +312,16 @@ 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, h, low = calculate_max_drawdown(df, date_col='open_date', value_col='profit') drawdown, hdate, ldate, hval, lval = calculate_max_drawdown(
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, float)
# High must be before low # High must be before low
assert h < low assert hdate < ldate
# High value must be higher than low value
assert hval > lval
assert drawdown == 0.091755 assert drawdown == 0.091755
df = DataFrame(zip(values[:5], dates[:5]), columns=['profit', 'open_date']) df = DataFrame(zip(values[:5], dates[:5]), columns=['profit', 'open_date'])