Merge pull request #3018 from freqtrade/max_drawdown
Max drawdown in plot-profit
This commit is contained in:
commit
33c1c8f726
@ -196,6 +196,7 @@ The first graph is good to get a grip of how the overall market progresses.
|
|||||||
|
|
||||||
The second graph will show if your algorithm works or doesn't.
|
The second graph will show if your algorithm works or doesn't.
|
||||||
Perhaps you want an algorithm that steadily makes small profits, or one that acts less often, but makes big swings.
|
Perhaps you want an algorithm that steadily makes small profits, or one that acts less often, but makes big swings.
|
||||||
|
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 third graph can be useful to spot outliers, events in pairs that cause profit spikes.
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ Helpers when analyzing backtest data
|
|||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, Union
|
from typing import Dict, Union, Tuple
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
@ -188,3 +188,28 @@ def create_cum_profit(df: pd.DataFrame, trades: pd.DataFrame, col_name: str,
|
|||||||
# FFill to get continuous
|
# FFill to get continuous
|
||||||
df[col_name] = df[col_name].ffill()
|
df[col_name] = df[col_name].ffill()
|
||||||
return df
|
return df
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_max_drawdown(trades: pd.DataFrame, *, date_col: str = 'close_time',
|
||||||
|
value_col: str = 'profitperc'
|
||||||
|
) -> Tuple[float, pd.Timestamp, pd.Timestamp]:
|
||||||
|
"""
|
||||||
|
Calculate max drawdown and the corresponding close dates
|
||||||
|
:param trades: DataFrame containing trades (requires columns close_time and profitperc)
|
||||||
|
:param date_col: Column in DataFrame to use for dates (defaults to 'close_time')
|
||||||
|
:param value_col: Column in DataFrame to use for values (defaults to 'profitperc')
|
||||||
|
:return: Tuple (float, highdate, lowdate) with absolute max drawdown, high and low time
|
||||||
|
:raise: ValueError if trade-dataframe was found empty.
|
||||||
|
"""
|
||||||
|
if len(trades) == 0:
|
||||||
|
raise ValueError("Trade dataframe empty.")
|
||||||
|
profit_results = trades.sort_values(date_col)
|
||||||
|
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']
|
||||||
|
|
||||||
|
high_date = profit_results.loc[max_drawdown_df['high_value'].idxmax(), date_col]
|
||||||
|
low_date = profit_results.loc[max_drawdown_df['drawdown'].idxmin(), date_col]
|
||||||
|
|
||||||
|
return abs(min(max_drawdown_df['drawdown'])), high_date, low_date
|
||||||
|
@ -5,7 +5,8 @@ from typing import Any, Dict, List
|
|||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
from freqtrade.configuration import TimeRange
|
from freqtrade.configuration import TimeRange
|
||||||
from freqtrade.data.btanalysis import (combine_tickers_with_mean,
|
from freqtrade.data.btanalysis import (calculate_max_drawdown,
|
||||||
|
combine_tickers_with_mean,
|
||||||
create_cum_profit,
|
create_cum_profit,
|
||||||
extract_trades_of_period, load_trades)
|
extract_trades_of_period, load_trades)
|
||||||
from freqtrade.data.converter import trim_dataframe
|
from freqtrade.data.converter import trim_dataframe
|
||||||
@ -111,6 +112,36 @@ def add_profit(fig, row, data: pd.DataFrame, column: str, name: str) -> make_sub
|
|||||||
return fig
|
return fig
|
||||||
|
|
||||||
|
|
||||||
|
def add_max_drawdown(fig, row, trades: pd.DataFrame, df_comb: pd.DataFrame) -> make_subplots:
|
||||||
|
"""
|
||||||
|
Add scatter points indicating max drawdown
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
max_drawdown, highdate, lowdate = calculate_max_drawdown(trades)
|
||||||
|
|
||||||
|
drawdown = go.Scatter(
|
||||||
|
x=[highdate, lowdate],
|
||||||
|
y=[
|
||||||
|
df_comb.loc[highdate, 'cum_profit'],
|
||||||
|
df_comb.loc[lowdate, 'cum_profit'],
|
||||||
|
],
|
||||||
|
mode='markers',
|
||||||
|
name=f"Max drawdown {max_drawdown:.2f}%",
|
||||||
|
text=f"Max drawdown {max_drawdown:.2f}%",
|
||||||
|
marker=dict(
|
||||||
|
symbol='square-open',
|
||||||
|
size=9,
|
||||||
|
line=dict(width=2),
|
||||||
|
color='green'
|
||||||
|
|
||||||
|
)
|
||||||
|
)
|
||||||
|
fig.add_trace(drawdown, row, 1)
|
||||||
|
except ValueError:
|
||||||
|
logger.warning("No trades found - not plotting max drawdown.")
|
||||||
|
return fig
|
||||||
|
|
||||||
|
|
||||||
def plot_trades(fig, trades: pd.DataFrame) -> make_subplots:
|
def plot_trades(fig, trades: pd.DataFrame) -> make_subplots:
|
||||||
"""
|
"""
|
||||||
Add trades to "fig"
|
Add trades to "fig"
|
||||||
@ -364,6 +395,7 @@ def generate_profit_graph(pairs: str, tickers: Dict[str, pd.DataFrame],
|
|||||||
|
|
||||||
fig.add_trace(avgclose, 1, 1)
|
fig.add_trace(avgclose, 1, 1)
|
||||||
fig = add_profit(fig, 2, df_comb, 'cum_profit', 'Profit')
|
fig = add_profit(fig, 2, df_comb, 'cum_profit', 'Profit')
|
||||||
|
fig = add_max_drawdown(fig, 2, trades, df_comb)
|
||||||
|
|
||||||
for pair in pairs:
|
for pair in pairs:
|
||||||
profit_col = f'cum_profit_{pair}'
|
profit_col = f'cum_profit_{pair}'
|
||||||
|
@ -2,15 +2,17 @@ from unittest.mock import MagicMock
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from arrow import Arrow
|
from arrow import Arrow
|
||||||
from pandas import DataFrame, DateOffset, to_datetime
|
from pandas import DataFrame, DateOffset, to_datetime, Timestamp
|
||||||
|
|
||||||
from freqtrade.configuration import TimeRange
|
from freqtrade.configuration import TimeRange
|
||||||
from freqtrade.data.btanalysis import (BT_DATA_COLUMNS,
|
from freqtrade.data.btanalysis import (BT_DATA_COLUMNS,
|
||||||
|
analyze_trade_parallelism,
|
||||||
|
calculate_max_drawdown,
|
||||||
combine_tickers_with_mean,
|
combine_tickers_with_mean,
|
||||||
create_cum_profit,
|
create_cum_profit,
|
||||||
extract_trades_of_period,
|
extract_trades_of_period,
|
||||||
load_backtest_data, load_trades,
|
load_backtest_data, load_trades,
|
||||||
load_trades_from_db, analyze_trade_parallelism)
|
load_trades_from_db)
|
||||||
from freqtrade.data.history import load_data, load_pair_history
|
from freqtrade.data.history import load_data, load_pair_history
|
||||||
from tests.test_persistence import create_mock_trades
|
from tests.test_persistence import create_mock_trades
|
||||||
|
|
||||||
@ -163,3 +165,17 @@ def test_create_cum_profit1(testdatadir):
|
|||||||
assert "cum_profits" in cum_profits.columns
|
assert "cum_profits" in cum_profits.columns
|
||||||
assert cum_profits.iloc[0]['cum_profits'] == 0
|
assert cum_profits.iloc[0]['cum_profits'] == 0
|
||||||
assert cum_profits.iloc[-1]['cum_profits'] == 0.0798005
|
assert cum_profits.iloc[-1]['cum_profits'] == 0.0798005
|
||||||
|
|
||||||
|
|
||||||
|
def test_calculate_max_drawdown(testdatadir):
|
||||||
|
filename = testdatadir / "backtest-result_test.json"
|
||||||
|
bt_data = load_backtest_data(filename)
|
||||||
|
drawdown, h, low = calculate_max_drawdown(bt_data)
|
||||||
|
assert isinstance(drawdown, float)
|
||||||
|
assert pytest.approx(drawdown) == 0.21142322
|
||||||
|
assert isinstance(h, Timestamp)
|
||||||
|
assert isinstance(low, Timestamp)
|
||||||
|
assert h == Timestamp('2018-01-24 14:25:00', tz='UTC')
|
||||||
|
assert low == Timestamp('2018-01-30 04:45:00', tz='UTC')
|
||||||
|
with pytest.raises(ValueError, match='Trade dataframe empty.'):
|
||||||
|
drawdown, h, low = calculate_max_drawdown(DataFrame())
|
||||||
|
@ -3,15 +3,16 @@ from copy import deepcopy
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
import plotly.graph_objects as go
|
import plotly.graph_objects as go
|
||||||
import pytest
|
import pytest
|
||||||
from plotly.subplots import make_subplots
|
from plotly.subplots import make_subplots
|
||||||
|
|
||||||
|
from freqtrade.commands import start_plot_dataframe, start_plot_profit
|
||||||
from freqtrade.configuration import TimeRange
|
from freqtrade.configuration import TimeRange
|
||||||
from freqtrade.data import history
|
from freqtrade.data import history
|
||||||
from freqtrade.data.btanalysis import create_cum_profit, load_backtest_data
|
from freqtrade.data.btanalysis import create_cum_profit, load_backtest_data
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.commands import start_plot_dataframe, start_plot_profit
|
|
||||||
from freqtrade.plot.plotting import (add_indicators, add_profit,
|
from freqtrade.plot.plotting import (add_indicators, add_profit,
|
||||||
create_plotconfig,
|
create_plotconfig,
|
||||||
generate_candlestick_graph,
|
generate_candlestick_graph,
|
||||||
@ -266,6 +267,7 @@ def test_generate_profit_graph(testdatadir):
|
|||||||
trades = load_backtest_data(filename)
|
trades = load_backtest_data(filename)
|
||||||
timerange = TimeRange.parse_timerange("20180110-20180112")
|
timerange = TimeRange.parse_timerange("20180110-20180112")
|
||||||
pairs = ["TRX/BTC", "ADA/BTC"]
|
pairs = ["TRX/BTC", "ADA/BTC"]
|
||||||
|
trades = trades[trades['close_time'] < pd.Timestamp('2018-01-12', tz='UTC')]
|
||||||
|
|
||||||
tickers = history.load_data(datadir=testdatadir,
|
tickers = history.load_data(datadir=testdatadir,
|
||||||
pairs=pairs,
|
pairs=pairs,
|
||||||
@ -283,13 +285,15 @@ def test_generate_profit_graph(testdatadir):
|
|||||||
assert fig.layout.yaxis3.title.text == "Profit"
|
assert fig.layout.yaxis3.title.text == "Profit"
|
||||||
|
|
||||||
figure = fig.layout.figure
|
figure = fig.layout.figure
|
||||||
assert len(figure.data) == 4
|
assert len(figure.data) == 5
|
||||||
|
|
||||||
avgclose = find_trace_in_fig_data(figure.data, "Avg close price")
|
avgclose = find_trace_in_fig_data(figure.data, "Avg close price")
|
||||||
assert isinstance(avgclose, go.Scatter)
|
assert isinstance(avgclose, go.Scatter)
|
||||||
|
|
||||||
profit = find_trace_in_fig_data(figure.data, "Profit")
|
profit = find_trace_in_fig_data(figure.data, "Profit")
|
||||||
assert isinstance(profit, go.Scatter)
|
assert isinstance(profit, go.Scatter)
|
||||||
|
profit = find_trace_in_fig_data(figure.data, "Max drawdown 0.00%")
|
||||||
|
assert isinstance(profit, go.Scatter)
|
||||||
|
|
||||||
for pair in pairs:
|
for pair in pairs:
|
||||||
profit_pair = find_trace_in_fig_data(figure.data, f"Profit {pair}")
|
profit_pair = find_trace_in_fig_data(figure.data, f"Profit {pair}")
|
||||||
|
Loading…
Reference in New Issue
Block a user