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.
|
||||
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.
|
||||
|
||||
|
@ -3,7 +3,7 @@ Helpers when analyzing backtest data
|
||||
"""
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Dict, Union
|
||||
from typing import Dict, Union, Tuple
|
||||
|
||||
import numpy as np
|
||||
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
|
||||
df[col_name] = df[col_name].ffill()
|
||||
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
|
||||
|
||||
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,
|
||||
extract_trades_of_period, load_trades)
|
||||
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
|
||||
|
||||
|
||||
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:
|
||||
"""
|
||||
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_profit(fig, 2, df_comb, 'cum_profit', 'Profit')
|
||||
fig = add_max_drawdown(fig, 2, trades, df_comb)
|
||||
|
||||
for pair in pairs:
|
||||
profit_col = f'cum_profit_{pair}'
|
||||
|
@ -2,15 +2,17 @@ from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
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.data.btanalysis import (BT_DATA_COLUMNS,
|
||||
analyze_trade_parallelism,
|
||||
calculate_max_drawdown,
|
||||
combine_tickers_with_mean,
|
||||
create_cum_profit,
|
||||
extract_trades_of_period,
|
||||
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 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.iloc[0]['cum_profits'] == 0
|
||||
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 unittest.mock import MagicMock
|
||||
|
||||
import pandas as pd
|
||||
import plotly.graph_objects as go
|
||||
import pytest
|
||||
from plotly.subplots import make_subplots
|
||||
|
||||
from freqtrade.commands import start_plot_dataframe, start_plot_profit
|
||||
from freqtrade.configuration import TimeRange
|
||||
from freqtrade.data import history
|
||||
from freqtrade.data.btanalysis import create_cum_profit, load_backtest_data
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.commands import start_plot_dataframe, start_plot_profit
|
||||
from freqtrade.plot.plotting import (add_indicators, add_profit,
|
||||
create_plotconfig,
|
||||
generate_candlestick_graph,
|
||||
@ -266,6 +267,7 @@ def test_generate_profit_graph(testdatadir):
|
||||
trades = load_backtest_data(filename)
|
||||
timerange = TimeRange.parse_timerange("20180110-20180112")
|
||||
pairs = ["TRX/BTC", "ADA/BTC"]
|
||||
trades = trades[trades['close_time'] < pd.Timestamp('2018-01-12', tz='UTC')]
|
||||
|
||||
tickers = history.load_data(datadir=testdatadir,
|
||||
pairs=pairs,
|
||||
@ -283,13 +285,15 @@ def test_generate_profit_graph(testdatadir):
|
||||
assert fig.layout.yaxis3.title.text == "Profit"
|
||||
|
||||
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")
|
||||
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 0.00%")
|
||||
assert isinstance(profit, go.Scatter)
|
||||
|
||||
for pair in pairs:
|
||||
profit_pair = find_trace_in_fig_data(figure.data, f"Profit {pair}")
|
||||
|
Loading…
Reference in New Issue
Block a user