diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 8364045da..bb2fd4591 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -390,6 +390,7 @@ def calculate_max_drawdown(trades: pd.DataFrame, *, date_col: str = 'close_date' low_val = max_drawdown_df.loc[idxmin, 'cumulative'] return abs(min(max_drawdown_df['drawdown'])), high_date, low_date, high_val, low_val +# TODO : is supposed to work only with long positions def calculate_trades_mdd(data: dict, trades: pd.DataFrame) -> float : """ Calculate Trades MDD (Max DrawDown) : @@ -409,57 +410,55 @@ def calculate_trades_mdd(data: dict, trades: pd.DataFrame) -> float : trades_mdd_pair_list = [] - for pair, df in data.items(): - if df is None: - break + for pair, df in data.items(): + if isinstance(df, pd.DataFrame): + # Gather the opening and closing trade dates into one Dates DataFrame + open_close_trades = trades.loc[trades['pair']==pair][["open_date","close_date"]] + open_close_trades = pd.concat( + [open_close_trades.rename(columns={'open_date':'date'})[['date']], + open_close_trades.rename(columns={'close_date':'date'})[['date']]] + ).sort_values(by='date') + + # Mark the dates and join it to the current candle dataframe. + # This allow to determine the open and close trade dates in the current + # candle dataframe. + open_close_trades['open_close_mark'] = 1 + data_join = df.set_index('date').join(open_close_trades.set_index('date')) + del open_close_trades - # Gather the opening and closing trade dates into one Dates DataFrame - open_close_trades = trades.loc[trades['pair']==pair][["open_date","close_date"]] - open_close_trades = pd.concat( - [open_close_trades.rename(columns={'open_date':'date'})[['date']], - open_close_trades.rename(columns={'close_date':'date'})[['date']]] - ).sort_values(by='date') - - # Mark the dates and join it to the current candle dataframe. - # This allow to determine the open and close trade dates in the current - # candle dataframe. - open_close_trades['open_close_mark'] = 1 - data_join = df.set_index('date').join(open_close_trades.set_index('date')) - del open_close_trades + # Gather, mark and join only the opening trade dates into the current candle + # dataframe. + # This allow to classify trades using the cumsum and split by classes + # with groupby in order to process a cummax on each trades independantly. + open_trades = trades.loc[trades['pair']==pair][["open_date"]] + open_trades = open_trades.rename(columns={'open_date':'date'}) + open_trades['open_mark'] = 1 + data_join = data_join.join(open_trades.set_index('date')) + del open_trades - # Gather, mark and join only the opening trade dates into the current candle - # dataframe. - # This allow to classify trades using the cumsum and split by classes - # with groupby in order to process a cummax on each trades independantly. - open_trades = trades.loc[trades['pair']==pair][["open_date"]] - open_trades = open_trades.rename(columns={'open_date':'date'}) - open_trades['open_mark'] = 1 - data_join = data_join.join(open_trades.set_index('date')) - del open_trades + # Set all unmarked date to 0 + data_join[["open_close_mark",'open_mark']] = data_join[ + ["open_close_mark",'open_mark']].fillna(0).astype(int) - # Set all unmarked date to 0 - data_join[["open_close_mark",'open_mark']] = data_join[ - ["open_close_mark",'open_mark']].fillna(0).astype(int) + # Mark with one all dates between an opening date trades and a closing date trades. + data_join['is_in_trade'] = data_join.open_close_mark.cumsum()&1 # &1 <=> %2 + data_join.loc[data_join['open_close_mark'] == 1, 'is_in_trade'] = 1 + + # Perform a cummax in each trades independtly + data_join['close_cummax'] = 0 + data_join['close_cummax'] = data_join.groupby( + data_join['open_mark'].cumsum() + )['close'].cummax() + data_join.loc[data_join['is_in_trade'] == 0, 'close_cummax'] = 0 - # Mark with one all dates between an opening date trades and a closing date trades. - data_join['is_in_trade'] = data_join.open_close_mark.cumsum()&1 # &1 <=> %2 - data_join.loc[data_join['open_close_mark'] == 1, 'is_in_trade'] = 1 - - # Perform a cummax in each trades independtly - data_join['close_cummax'] = 0 - data_join['close_cummax'] = data_join.groupby( - data_join['open_mark'].cumsum() - )['close'].cummax() - data_join.loc[data_join['is_in_trade'] == 0, 'close_cummax'] = 0 + # Compute the drawdown at each time of each trades + data_join = data_join.rename(columns={'open_mark':'drawdown'}) + data_join.loc[data_join['is_in_trade'] == 1, 'drawdown'] = \ + (data_join['close_cummax'] - data_join['close']) \ + / data_join['close_cummax'] - # Compute the drawdown at each time of each trades - data_join = data_join.rename(columns={'open_mark':'drawdown'}) - data_join.loc[data_join['is_in_trade'] == 1, 'drawdown'] = \ - (data_join['close_cummax'] - data_join['close']) \ - / data_join['close_cummax'] - - mdd_pair = data_join['drawdown'].max() - trades_mdd_pair_list.append(mdd_pair) + mdd_pair = data_join['drawdown'].max() + trades_mdd_pair_list.append(mdd_pair) if trades_mdd_pair_list == []: raise ValueError("All dataframe in candle data are None") diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index 1dcd04a80..384a71e7d 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -1,7 +1,7 @@ from math import isclose from pathlib import Path from unittest.mock import MagicMock - +import numpy as np import pytest from arrow import Arrow from pandas import DataFrame, DateOffset, Timestamp, to_datetime @@ -10,7 +10,7 @@ from freqtrade.configuration import TimeRange from freqtrade.constants import LAST_BT_RESULT_FN from freqtrade.data.btanalysis import (BT_DATA_COLUMNS, BT_DATA_COLUMNS_MID, BT_DATA_COLUMNS_OLD, analyze_trade_parallelism, calculate_csum, - calculate_market_change, calculate_max_drawdown, + calculate_market_change, calculate_max_drawdown, calculate_trades_mdd, combine_dataframes_with_mean, create_cum_profit, extract_trades_of_period, get_latest_backtest_filename, get_latest_hyperopt_file, load_backtest_data, load_trades, @@ -332,3 +332,15 @@ 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.'): calculate_max_drawdown(df, date_col='open_date', value_col='profit') + +def test_calculate_trades_mdd(testdatadir): + backtest_file = testdatadir / "backtest-result_test.json" + trades = load_backtest_data(backtest_file) + pairlist = set(trades["pair"]) + + with pytest.raises(ValueError, match='All dataframe in candle data are None'): + calculate_trades_mdd({"BTC/BUSD" : None}, trades) + + data = load_data(datadir=testdatadir, pairs=pairlist, timeframe='5m') + trades_mdd = calculate_trades_mdd(data, trades) + assert np.round(trades_mdd, 6) == 0.138943 \ No newline at end of file