diff --git a/docs/assets/plot-profit.png b/docs/assets/plot-profit.png index 88d69a2d4..e9fe6c341 100644 Binary files a/docs/assets/plot-profit.png and b/docs/assets/plot-profit.png differ diff --git a/docs/plotting.md b/docs/plotting.md index b2d7654f6..315dbc236 100644 --- a/docs/plotting.md +++ b/docs/plotting.md @@ -283,6 +283,8 @@ The `plot-profit` subcommand shows an interactive graph with three plots: * The summarized profit made by backtesting. Note that this is not the real-world profit, but more of an estimate. * Profit for each individual pair. +* Parallelism of trades. +* Underwater (Periods of drawdown). The first graph is good to get a grip of how the overall market progresses. @@ -292,6 +294,8 @@ This graph will also highlight the start (and end) of the Max drawdown period. The third graph can be useful to spot outliers, events in pairs that cause profit spikes. +The forth graph can help you analyze trade parallelism, showing how often max_open_trades have been maxed out. + Possible options for the `freqtrade plot-profit` subcommand: ``` diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 560dd42f1..56c16f966 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -361,6 +361,36 @@ def create_cum_profit(df: pd.DataFrame, trades: pd.DataFrame, col_name: str, return df +def _calc_drawdown_series(profit_results: pd.DataFrame, *, date_col: str, value_col: str + ) -> pd.DataFrame: + max_drawdown_df = pd.DataFrame() + max_drawdown_df['cumulative'] = profit_results[value_col].cumsum() + max_drawdown_df['high_value'] = max_drawdown_df['cumulative'].cummax() + max_drawdown_df['drawdown'] = max_drawdown_df['cumulative'] - max_drawdown_df['high_value'] + max_drawdown_df['date'] = profit_results.loc[:, date_col] + return max_drawdown_df + + +def calculate_underwater(trades: pd.DataFrame, *, date_col: str = 'close_date', + value_col: str = 'profit_ratio' + ): + """ + 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_ratio') + :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. + """ + if len(trades) == 0: + raise ValueError("Trade dataframe empty.") + profit_results = trades.sort_values(date_col).reset_index(drop=True) + max_drawdown_df = _calc_drawdown_series(profit_results, date_col=date_col, value_col=value_col) + + return max_drawdown_df + + def calculate_max_drawdown(trades: pd.DataFrame, *, date_col: str = 'close_date', value_col: str = 'profit_ratio' ) -> Tuple[float, pd.Timestamp, pd.Timestamp, float, float]: @@ -376,10 +406,7 @@ def calculate_max_drawdown(trades: pd.DataFrame, *, date_col: str = 'close_date' if len(trades) == 0: raise ValueError("Trade dataframe empty.") profit_results = trades.sort_values(date_col).reset_index(drop=True) - max_drawdown_df = pd.DataFrame() - max_drawdown_df['cumulative'] = profit_results[value_col].cumsum() - max_drawdown_df['high_value'] = max_drawdown_df['cumulative'].cummax() - max_drawdown_df['drawdown'] = max_drawdown_df['cumulative'] - max_drawdown_df['high_value'] + max_drawdown_df = _calc_drawdown_series(profit_results, date_col=date_col, value_col=value_col) idxmin = max_drawdown_df['drawdown'].idxmin() if idxmin == 0: diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index fd5317e27..c0888808f 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -5,7 +5,8 @@ from typing import Any, Dict, List import pandas as pd from freqtrade.configuration import TimeRange -from freqtrade.data.btanalysis import (calculate_max_drawdown, combine_dataframes_with_mean, +from freqtrade.data.btanalysis import (analyze_trade_parallelism, calculate_max_drawdown, + calculate_underwater, combine_dataframes_with_mean, create_cum_profit, extract_trades_of_period, load_trades) from freqtrade.data.converter import trim_dataframe from freqtrade.data.dataprovider import DataProvider @@ -185,6 +186,48 @@ def add_max_drawdown(fig, row, trades: pd.DataFrame, df_comb: pd.DataFrame, return fig +def add_underwater(fig, row, trades: pd.DataFrame) -> make_subplots: + """ + Add underwater plot + """ + try: + underwater = calculate_underwater(trades, value_col="profit_abs") + + underwater = go.Scatter( + x=underwater['date'], + y=underwater['drawdown'], + name="Underwater Plot", + fill='tozeroy', + fillcolor='#cc362b', + line={'color': '#cc362b'}, + ) + fig.add_trace(underwater, row, 1) + except ValueError: + logger.warning("No trades found - not plotting underwater plot") + return fig + + +def add_parallelism(fig, row, trades: pd.DataFrame, timeframe: str) -> make_subplots: + """ + Add Chart showing trade parallelism + """ + try: + result = analyze_trade_parallelism(trades, timeframe) + + drawdown = go.Scatter( + x=result.index, + y=result['open_trades'], + name="Parallel trades", + fill='tozeroy', + fillcolor='#242222', + line={'color': '#242222'}, + ) + fig.add_trace(drawdown, row, 1) + except ValueError: + logger.warning("No trades found - not plotting Parallelism.") + return fig + + def plot_trades(fig, trades: pd.DataFrame) -> make_subplots: """ Add trades to "fig" @@ -482,20 +525,30 @@ def generate_profit_graph(pairs: str, data: Dict[str, pd.DataFrame], name='Avg close price', ) - fig = make_subplots(rows=3, cols=1, shared_xaxes=True, - row_width=[1, 1, 1], + fig = make_subplots(rows=5, cols=1, shared_xaxes=True, + row_heights=[1, 1, 1, 0.5, 1], vertical_spacing=0.05, - subplot_titles=["AVG Close Price", "Combined Profit", "Profit per pair"]) + subplot_titles=[ + "AVG Close Price", + "Combined Profit", + "Profit per pair", + "Parallelism", + "Underwater", + ]) fig['layout'].update(title="Freqtrade Profit plot") fig['layout']['yaxis1'].update(title='Price') fig['layout']['yaxis2'].update(title=f'Profit {stake_currency}') fig['layout']['yaxis3'].update(title=f'Profit {stake_currency}') + fig['layout']['yaxis4'].update(title='Trade count') + fig['layout']['yaxis5'].update(title='Underwater Plot') fig['layout']['xaxis']['rangeslider'].update(visible=False) fig.update_layout(modebar_add=["v1hovermode", "toggleSpikeLines"]) fig.add_trace(avgclose, 1, 1) fig = add_profit(fig, 2, df_comb, 'cum_profit', 'Profit') fig = add_max_drawdown(fig, 2, trades, df_comb, timeframe) + fig = add_parallelism(fig, 4, trades, timeframe) + fig = add_underwater(fig, 5, trades) for pair in pairs: profit_col = f'cum_profit_{pair}' diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index b06dfce0a..47f1b8849 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -11,10 +11,10 @@ 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, - combine_dataframes_with_mean, create_cum_profit, - extract_trades_of_period, get_latest_backtest_filename, - get_latest_hyperopt_file, load_backtest_data, load_trades, - load_trades_from_db) + calculate_underwater, combine_dataframes_with_mean, + create_cum_profit, extract_trades_of_period, + get_latest_backtest_filename, get_latest_hyperopt_file, + load_backtest_data, load_trades, load_trades_from_db) from freqtrade.data.history import load_data, load_pair_history from tests.conftest import create_mock_trades from tests.conftest_trades import MOCK_TRADE_COUNT @@ -291,9 +291,16 @@ def test_calculate_max_drawdown(testdatadir): 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') + + underwater = calculate_underwater(bt_data) + assert isinstance(underwater, DataFrame) + with pytest.raises(ValueError, match='Trade dataframe empty.'): drawdown, hdate, lowdate, hval, lval = calculate_max_drawdown(DataFrame()) + with pytest.raises(ValueError, match='Trade dataframe empty.'): + calculate_underwater(DataFrame()) + def test_calculate_csum(testdatadir): filename = testdatadir / "backtest-result_test.json" diff --git a/tests/test_plotting.py b/tests/test_plotting.py index 8a40f4a20..40a76d04e 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -336,15 +336,20 @@ def test_generate_profit_graph(testdatadir): assert fig.layout.yaxis3.title.text == "Profit BTC" figure = fig.layout.figure - assert len(figure.data) == 5 + assert len(figure.data) == 7 avgclose = find_trace_in_fig_data(figure.data, "Avg close price") assert isinstance(avgclose, go.Scatter) profit = find_trace_in_fig_data(figure.data, "Profit") assert isinstance(profit, go.Scatter) - profit = find_trace_in_fig_data(figure.data, "Max drawdown 10.45%") - assert isinstance(profit, go.Scatter) + drawdown = find_trace_in_fig_data(figure.data, "Max drawdown 10.45%") + assert isinstance(drawdown, go.Scatter) + parallel = find_trace_in_fig_data(figure.data, "Parallel trades") + assert isinstance(parallel, go.Scatter) + + underwater = find_trace_in_fig_data(figure.data, "Underwater Plot") + assert isinstance(underwater, go.Scatter) for pair in pairs: profit_pair = find_trace_in_fig_data(figure.data, f"Profit {pair}")