From 47a6ef4f00707e57fac348035d6092c9bd5d7b71 Mon Sep 17 00:00:00 2001 From: Nicolas Papp Date: Sun, 10 Apr 2022 12:53:47 -0300 Subject: [PATCH 01/18] Max relative drawdown --- docs/hyperopt.md | 7 ++- freqtrade/constants.py | 3 +- freqtrade/data/btanalysis.py | 23 ++++++---- .../hyperopt_loss_max_drawdown_relative.py | 45 +++++++++++++++++++ freqtrade/optimize/optimize_reports.py | 9 +++- 5 files changed, 73 insertions(+), 14 deletions(-) create mode 100644 freqtrade/optimize/hyperopt_loss_max_drawdown_relative.py diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 3f613a208..bab062fad 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -116,7 +116,9 @@ optional arguments: ShortTradeDurHyperOptLoss, OnlyProfitHyperOptLoss, SharpeHyperOptLoss, SharpeHyperOptLossDaily, SortinoHyperOptLoss, SortinoHyperOptLossDaily, - CalmarHyperOptLoss, MaxDrawDownHyperOptLoss, ProfitDrawDownHyperOptLoss + CalmarHyperOptLoss, MaxDrawDownHyperOptLoss, + MaxDrawDownRelativeHyperOptLoss, + ProfitDrawDownHyperOptLoss --disable-param-export Disable automatic hyperopt parameter export. --ignore-missing-spaces, --ignore-unparameterized-spaces @@ -563,7 +565,8 @@ Currently, the following loss functions are builtin: * `SharpeHyperOptLossDaily` - optimizes Sharpe Ratio calculated on **daily** trade returns relative to standard deviation. * `SortinoHyperOptLoss` - optimizes Sortino Ratio calculated on trade returns relative to **downside** standard deviation. * `SortinoHyperOptLossDaily` - optimizes Sortino Ratio calculated on **daily** trade returns relative to **downside** standard deviation. -* `MaxDrawDownHyperOptLoss` - Optimizes Maximum drawdown. +* `MaxDrawDownHyperOptLoss` - Optimizes Maximum absolute drawdown. +* `MaxDrawDownRelativeHyperOptLoss` - Similar as the above, but also optimizes Maximum relative drawdown. * `CalmarHyperOptLoss` - Optimizes Calmar Ratio calculated on trade returns relative to max drawdown. * `ProfitDrawDownHyperOptLoss` - Optimizes by max Profit & min Drawdown objective. `DRAWDOWN_MULT` variable within the hyperoptloss file can be adjusted to be stricter or more flexible on drawdown purposes. diff --git a/freqtrade/constants.py b/freqtrade/constants.py index a06e2771f..e2d4d9a13 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -28,7 +28,8 @@ HYPEROPT_LOSS_BUILTIN = ['ShortTradeDurHyperOptLoss', 'OnlyProfitHyperOptLoss', 'SharpeHyperOptLoss', 'SharpeHyperOptLossDaily', 'SortinoHyperOptLoss', 'SortinoHyperOptLossDaily', 'CalmarHyperOptLoss', - 'MaxDrawDownHyperOptLoss', 'ProfitDrawDownHyperOptLoss'] + 'MaxDrawDownHyperOptLoss', 'MaxDrawDownRelativeHyperOptLoss', + 'ProfitDrawDownHyperOptLoss'] AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', 'AgeFilter', 'OffsetFilter', 'PerformanceFilter', 'PrecisionFilter', 'PriceFilter', 'RangeStabilityFilter', diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index c8654cfda..66d7b4ad5 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -441,18 +441,22 @@ 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: +def _calc_drawdown_series(profit_results: pd.DataFrame, *, date_col: str, value_col: str, + starting_balance : Optional[float] = 0.0) -> 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] + if starting_balance: + cumulative_balance = starting_balance + max_drawdown_df['cumulative'] + max_balance = starting_balance + max_drawdown_df['high_value'] + max_drawdown_df['drawdown_relative'] = ((max_balance - cumulative_balance) / max_balance) return max_drawdown_df def calculate_underwater(trades: pd.DataFrame, *, date_col: str = 'close_date', - value_col: str = 'profit_ratio' + value_col: str = 'profit_ratio', starting_balance : Optional[float] = 0.0 ): """ Calculate max drawdown and the corresponding close dates @@ -466,13 +470,14 @@ def calculate_underwater(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 = _calc_drawdown_series(profit_results, date_col=date_col, value_col=value_col) + max_drawdown_df = _calc_drawdown_series(profit_results, date_col=date_col, value_col=value_col, starting_balance=starting_balance) return max_drawdown_df def calculate_max_drawdown(trades: pd.DataFrame, *, date_col: str = 'close_date', - value_col: str = 'profit_abs', starting_balance: float = 0 + value_col: str = 'profit_abs', starting_balance: float = 0, + relative: bool = False ) -> Tuple[float, pd.Timestamp, pd.Timestamp, float, float, float]: """ Calculate max drawdown and the corresponding close dates @@ -488,9 +493,9 @@ 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 = _calc_drawdown_series(profit_results, date_col=date_col, value_col=value_col) + max_drawdown_df = _calc_drawdown_series(profit_results, date_col=date_col, value_col=value_col, starting_balance=starting_balance) - idxmin = max_drawdown_df['drawdown'].idxmin() + idxmin = max_drawdown_df['drawdown_relative'].idxmax() if relative else max_drawdown_df['drawdown'].idxmin() if idxmin == 0: raise ValueError("No losing trade, therefore no drawdown.") high_date = profit_results.loc[max_drawdown_df.iloc[:idxmin]['high_value'].idxmax(), date_col] @@ -499,8 +504,8 @@ def calculate_max_drawdown(trades: pd.DataFrame, *, date_col: str = 'close_date' ['high_value'].idxmax(), 'cumulative'] low_val = max_drawdown_df.loc[idxmin, 'cumulative'] max_drawdown_rel = 0.0 - if high_val + starting_balance != 0: - max_drawdown_rel = (high_val - low_val) / (high_val + starting_balance) + if starting_balance != 0: + max_drawdown_rel = max_drawdown_df.loc[idxmin, 'drawdown_relative'] return ( abs(min(max_drawdown_df['drawdown'])), diff --git a/freqtrade/optimize/hyperopt_loss_max_drawdown_relative.py b/freqtrade/optimize/hyperopt_loss_max_drawdown_relative.py new file mode 100644 index 000000000..c4dd843b8 --- /dev/null +++ b/freqtrade/optimize/hyperopt_loss_max_drawdown_relative.py @@ -0,0 +1,45 @@ +""" +MaxDrawDownRelativeHyperOptLoss + +This module defines the alternative HyperOptLoss class which can be used for +Hyperoptimization. +""" +from datetime import datetime +from typing import Dict + +from pandas import DataFrame + +from freqtrade.data.btanalysis import calculate_underwater, calculate_max_drawdown +from freqtrade.optimize.hyperopt import IHyperOptLoss + + +class MaxDrawDownRelativeHyperOptLoss(IHyperOptLoss): + + """ + Defines the loss function for hyperopt. + + This implementation optimizes for max draw down and profit + Less max drawdown more profit -> Lower return value + """ + + @staticmethod + def hyperopt_loss_function(results: DataFrame, config: Dict, + *args, **kwargs) -> float: + + """ + Objective function. + + Uses profit ratio weighted max_drawdown when drawdown is available. + Otherwise directly optimizes profit ratio. + """ + total_profit = results['profit_abs'].sum() + try: + drawdown_df = calculate_underwater(results, value_col='profit_abs', starting_balance=config['available_capital']) + max_drawdown = abs(min(drawdown_df['drawdown'])) + relative_drawdown = max(drawdown_df['drawdown_relative']) + if max_drawdown == 0: + return -total_profit + return -total_profit / max_drawdown / relative_drawdown + except (Exception, ValueError): + return -total_profit + diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 1966c7ad1..2bf09d71b 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -474,9 +474,12 @@ def generate_strategy_stats(pairlist: List[str], (drawdown_abs, drawdown_start, drawdown_end, high_val, low_val, max_drawdown) = calculate_max_drawdown( results, value_col='profit_abs', starting_balance=start_balance) + (_, _, _, _, _, max_relative_drawdown) = calculate_max_drawdown( + results, value_col='profit_abs', starting_balance=start_balance, relative=True) strat_stats.update({ 'max_drawdown': max_drawdown_legacy, # Deprecated - do not use 'max_drawdown_account': max_drawdown, + 'max_relative_drawdown': max_relative_drawdown, 'max_drawdown_abs': drawdown_abs, 'drawdown_start': drawdown_start.strftime(DATETIME_PRINT_FORMAT), 'drawdown_start_ts': drawdown_start.timestamp() * 1000, @@ -497,6 +500,7 @@ def generate_strategy_stats(pairlist: List[str], strat_stats.update({ 'max_drawdown': 0.0, 'max_drawdown_account': 0.0, + 'max_relative_drawdown': 0.0, 'max_drawdown_abs': 0.0, 'max_drawdown_low': 0.0, 'max_drawdown_high': 0.0, @@ -760,10 +764,11 @@ def text_table_add_metrics(strat_results: Dict) -> str: strat_results['stake_currency'])), # Compatibility to show old hyperopt results - ('Drawdown (Account)', f"{strat_results['max_drawdown_account']:.2%}") + ('Max % of account underwater', f"{strat_results['max_relative_drawdown']:.2%}"), + ('Absolute Drawdown (Account)', f"{strat_results['max_drawdown_account']:.2%}") if 'max_drawdown_account' in strat_results else ( 'Drawdown', f"{strat_results['max_drawdown']:.2%}"), - ('Drawdown', round_coin_value(strat_results['max_drawdown_abs'], + ('Absolute Drawdown', round_coin_value(strat_results['max_drawdown_abs'], strat_results['stake_currency'])), ('Drawdown high', round_coin_value(strat_results['max_drawdown_high'], strat_results['stake_currency'])), From c8e468783393d44c2bb5395e2994c0950720ddc0 Mon Sep 17 00:00:00 2001 From: Nicolas Papp Date: Mon, 11 Apr 2022 16:41:48 -0300 Subject: [PATCH 02/18] Plots and hyperopt --- freqtrade/data/btanalysis.py | 3 ++ freqtrade/plot/plotting.py | 44 ++++++++++++++++++++--------- tests/optimize/test_hyperoptloss.py | 1 + 3 files changed, 34 insertions(+), 14 deletions(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 3c54b0eeb..6858deb69 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -445,6 +445,9 @@ def _calc_drawdown_series(profit_results: pd.DataFrame, *, date_col: str, value_ cumulative_balance = starting_balance + max_drawdown_df['cumulative'] max_balance = starting_balance + max_drawdown_df['high_value'] max_drawdown_df['drawdown_relative'] = ((max_balance - cumulative_balance) / max_balance) + else: + # This is not completely accurate, + max_drawdown_df['drawdown_relative'] = ((max_drawdown_df['high_value'] - max_drawdown_df['cumulative']) / max_drawdown_df['high_value']) return max_drawdown_df diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 747248be7..3d651c1d9 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -1,6 +1,7 @@ import logging from pathlib import Path from typing import Any, Dict, List, Optional +from numpy import number import pandas as pd @@ -158,12 +159,12 @@ def add_profit(fig, row, data: pd.DataFrame, column: str, name: str) -> make_sub def add_max_drawdown(fig, row, trades: pd.DataFrame, df_comb: pd.DataFrame, - timeframe: str) -> make_subplots: + timeframe: str, starting_balance: number) -> make_subplots: """ Add scatter points indicating max drawdown """ try: - _, highdate, lowdate, _, _, max_drawdown = calculate_max_drawdown(trades) + _, highdate, lowdate, _, _, max_drawdown = calculate_max_drawdown(trades, starting_balance=starting_balance) drawdown = go.Scatter( x=[highdate, lowdate], @@ -188,22 +189,33 @@ 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: +def add_underwater(fig, row, trades: pd.DataFrame, starting_balance: number) -> make_subplots: """ - Add underwater plot + Add underwater plots """ try: - underwater = calculate_underwater(trades, value_col="profit_abs") + underwater = calculate_underwater(trades, value_col="profit_abs", starting_balance=starting_balance) - underwater = go.Scatter( + underwater_plot = go.Scatter( x=underwater['date'], y=underwater['drawdown'], name="Underwater Plot", fill='tozeroy', fillcolor='#cc362b', - line={'color': '#cc362b'}, + line={'color': '#cc362b'} ) - fig.add_trace(underwater, row, 1) + + underwater_plot_relative = go.Scatter( + x=underwater['date'], + y=(-underwater['drawdown_relative']), + name="Underwater Plot (%)", + fill='tozeroy', + fillcolor='green', + line={'color': 'green'} + ) + + fig.add_trace(underwater_plot, row, 1) + fig.add_trace(underwater_plot_relative, row+1, 1) except ValueError: logger.warning("No trades found - not plotting underwater plot") return fig @@ -506,7 +518,8 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra def generate_profit_graph(pairs: str, data: Dict[str, pd.DataFrame], - trades: pd.DataFrame, timeframe: str, stake_currency: str) -> go.Figure: + trades: pd.DataFrame, timeframe: str, stake_currency: str, + starting_balance: number) -> go.Figure: # Combine close-values for all pairs, rename columns to "pair" try: df_comb = combine_dataframes_with_mean(data, "close") @@ -530,8 +543,8 @@ def generate_profit_graph(pairs: str, data: Dict[str, pd.DataFrame], name='Avg close price', ) - fig = make_subplots(rows=5, cols=1, shared_xaxes=True, - row_heights=[1, 1, 1, 0.5, 1], + fig = make_subplots(rows=6, cols=1, shared_xaxes=True, + row_heights=[1, 1, 1, 0.5, 0.75, 0.75], vertical_spacing=0.05, subplot_titles=[ "AVG Close Price", @@ -539,6 +552,7 @@ def generate_profit_graph(pairs: str, data: Dict[str, pd.DataFrame], "Profit per pair", "Parallelism", "Underwater", + "Relative Drawdown", ]) fig['layout'].update(title="Freqtrade Profit plot") fig['layout']['yaxis1'].update(title='Price') @@ -546,14 +560,16 @@ def generate_profit_graph(pairs: str, data: Dict[str, pd.DataFrame], 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']['yaxis6'].update(title='Underwater Plot Relative (%)', tickformat=',.2%') 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_max_drawdown(fig, 2, trades, df_comb, timeframe, starting_balance) fig = add_parallelism(fig, 4, trades, timeframe) - fig = add_underwater(fig, 5, trades) + # Two rows consumed + fig = add_underwater(fig, 5, trades, starting_balance) for pair in pairs: profit_col = f'cum_profit_{pair}' @@ -668,7 +684,7 @@ def plot_profit(config: Dict[str, Any]) -> None: # this could be useful to gauge the overall market trend fig = generate_profit_graph(plot_elements['pairs'], plot_elements['ohlcv'], trades, config['timeframe'], - config.get('stake_currency', '')) + config.get('stake_currency', ''), config['available_capital']) store_plot_file(fig, filename='freqtrade-profit-plot.html', directory=config['user_data_dir'] / 'plot', auto_open=config.get('plot_auto_open', False)) diff --git a/tests/optimize/test_hyperoptloss.py b/tests/optimize/test_hyperoptloss.py index e3f6daf6c..aac02305e 100644 --- a/tests/optimize/test_hyperoptloss.py +++ b/tests/optimize/test_hyperoptloss.py @@ -85,6 +85,7 @@ def test_loss_calculation_has_limited_profit(hyperopt_conf, hyperopt_results) -> "SharpeHyperOptLoss", "SharpeHyperOptLossDaily", "MaxDrawDownHyperOptLoss", + "MaxDrawDownRelativeHyperOptLoss", "CalmarHyperOptLoss", "ProfitDrawDownHyperOptLoss", From 0f943c482b11a8cd7490fd7da62027d6652e1312 Mon Sep 17 00:00:00 2001 From: Nicolas Papp Date: Sat, 23 Apr 2022 13:15:14 -0300 Subject: [PATCH 03/18] PEP8 code compliance --- freqtrade/data/btanalysis.py | 26 ++++++++++++++----- .../hyperopt_loss_max_drawdown_relative.py | 10 ++++--- freqtrade/optimize/optimize_reports.py | 2 +- freqtrade/plot/plotting.py | 15 ++++++++--- 4 files changed, 37 insertions(+), 16 deletions(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 6858deb69..3803beb70 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -435,7 +435,7 @@ def create_cum_profit(df: pd.DataFrame, trades: pd.DataFrame, col_name: str, def _calc_drawdown_series(profit_results: pd.DataFrame, *, date_col: str, value_col: str, - starting_balance : Optional[float] = 0.0) -> pd.DataFrame: + starting_balance: Optional[float] = 0.0) -> 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() @@ -446,13 +446,15 @@ def _calc_drawdown_series(profit_results: pd.DataFrame, *, date_col: str, value_ max_balance = starting_balance + max_drawdown_df['high_value'] max_drawdown_df['drawdown_relative'] = ((max_balance - cumulative_balance) / max_balance) else: - # This is not completely accurate, - max_drawdown_df['drawdown_relative'] = ((max_drawdown_df['high_value'] - max_drawdown_df['cumulative']) / max_drawdown_df['high_value']) + # This is not completely accurate + max_drawdown_df['drawdown_relative'] = ( + (max_drawdown_df['high_value'] - max_drawdown_df['cumulative']) + / max_drawdown_df['high_value']) return max_drawdown_df def calculate_underwater(trades: pd.DataFrame, *, date_col: str = 'close_date', - value_col: str = 'profit_ratio', starting_balance : Optional[float] = 0.0 + value_col: str = 'profit_ratio', starting_balance: Optional[float] = 0.0 ): """ Calculate max drawdown and the corresponding close dates @@ -466,7 +468,11 @@ def calculate_underwater(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 = _calc_drawdown_series(profit_results, date_col=date_col, value_col=value_col, starting_balance=starting_balance) + max_drawdown_df = _calc_drawdown_series( + profit_results, + date_col=date_col, + value_col=value_col, + starting_balance=starting_balance) return max_drawdown_df @@ -489,9 +495,15 @@ 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 = _calc_drawdown_series(profit_results, date_col=date_col, value_col=value_col, starting_balance=starting_balance) + max_drawdown_df = _calc_drawdown_series( + profit_results, + date_col=date_col, + value_col=value_col, + starting_balance=starting_balance + ) - idxmin = max_drawdown_df['drawdown_relative'].idxmax() if relative else max_drawdown_df['drawdown'].idxmin() + idxmin = max_drawdown_df['drawdown_relative'].idxmax() if relative \ + else max_drawdown_df['drawdown'].idxmin() if idxmin == 0: raise ValueError("No losing trade, therefore no drawdown.") high_date = profit_results.loc[max_drawdown_df.iloc[:idxmin]['high_value'].idxmax(), date_col] diff --git a/freqtrade/optimize/hyperopt_loss_max_drawdown_relative.py b/freqtrade/optimize/hyperopt_loss_max_drawdown_relative.py index c4dd843b8..62fe76ee6 100644 --- a/freqtrade/optimize/hyperopt_loss_max_drawdown_relative.py +++ b/freqtrade/optimize/hyperopt_loss_max_drawdown_relative.py @@ -4,12 +4,11 @@ MaxDrawDownRelativeHyperOptLoss This module defines the alternative HyperOptLoss class which can be used for Hyperoptimization. """ -from datetime import datetime from typing import Dict from pandas import DataFrame -from freqtrade.data.btanalysis import calculate_underwater, calculate_max_drawdown +from freqtrade.data.btanalysis import calculate_underwater from freqtrade.optimize.hyperopt import IHyperOptLoss @@ -34,7 +33,11 @@ class MaxDrawDownRelativeHyperOptLoss(IHyperOptLoss): """ total_profit = results['profit_abs'].sum() try: - drawdown_df = calculate_underwater(results, value_col='profit_abs', starting_balance=config['available_capital']) + drawdown_df = calculate_underwater( + results, + value_col='profit_abs', + starting_balance=config['available_capital'] + ) max_drawdown = abs(min(drawdown_df['drawdown'])) relative_drawdown = max(drawdown_df['drawdown_relative']) if max_drawdown == 0: @@ -42,4 +45,3 @@ class MaxDrawDownRelativeHyperOptLoss(IHyperOptLoss): return -total_profit / max_drawdown / relative_drawdown except (Exception, ValueError): return -total_profit - diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 2d6e17468..32d16a235 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -769,7 +769,7 @@ def text_table_add_metrics(strat_results: Dict) -> str: if 'max_drawdown_account' in strat_results else ( 'Drawdown', f"{strat_results['max_drawdown']:.2%}"), ('Absolute Drawdown', round_coin_value(strat_results['max_drawdown_abs'], - strat_results['stake_currency'])), + strat_results['stake_currency'])), ('Drawdown high', round_coin_value(strat_results['max_drawdown_high'], strat_results['stake_currency'])), ('Drawdown low', round_coin_value(strat_results['max_drawdown_low'], diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 3d651c1d9..ed403e09f 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -164,7 +164,10 @@ def add_max_drawdown(fig, row, trades: pd.DataFrame, df_comb: pd.DataFrame, Add scatter points indicating max drawdown """ try: - _, highdate, lowdate, _, _, max_drawdown = calculate_max_drawdown(trades, starting_balance=starting_balance) + _, highdate, lowdate, _, _, max_drawdown = calculate_max_drawdown( + trades, + starting_balance=starting_balance + ) drawdown = go.Scatter( x=[highdate, lowdate], @@ -194,7 +197,11 @@ def add_underwater(fig, row, trades: pd.DataFrame, starting_balance: number) -> Add underwater plots """ try: - underwater = calculate_underwater(trades, value_col="profit_abs", starting_balance=starting_balance) + underwater = calculate_underwater( + trades, + value_col="profit_abs", + starting_balance=starting_balance + ) underwater_plot = go.Scatter( x=underwater['date'], @@ -213,9 +220,9 @@ def add_underwater(fig, row, trades: pd.DataFrame, starting_balance: number) -> fillcolor='green', line={'color': 'green'} ) - + fig.add_trace(underwater_plot, row, 1) - fig.add_trace(underwater_plot_relative, row+1, 1) + fig.add_trace(underwater_plot_relative, row + 1, 1) except ValueError: logger.warning("No trades found - not plotting underwater plot") return fig From 086cc6be931faa996a1e139a0622fae9ea944e4a Mon Sep 17 00:00:00 2001 From: Nicolas Papp Date: Sun, 24 Apr 2022 17:37:09 -0300 Subject: [PATCH 04/18] Correction on tests --- freqtrade/data/btanalysis.py | 4 +--- tests/test_plotting.py | 16 +++++++++++++--- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 3803beb70..6911941e6 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -511,9 +511,7 @@ def calculate_max_drawdown(trades: pd.DataFrame, *, date_col: str = 'close_date' high_val = max_drawdown_df.loc[max_drawdown_df.iloc[:idxmin] ['high_value'].idxmax(), 'cumulative'] low_val = max_drawdown_df.loc[idxmin, 'cumulative'] - max_drawdown_rel = 0.0 - if starting_balance != 0: - max_drawdown_rel = max_drawdown_df.loc[idxmin, 'drawdown_relative'] + max_drawdown_rel = max_drawdown_df.loc[idxmin, 'drawdown_relative'] return ( abs(min(max_drawdown_df['drawdown'])), diff --git a/tests/test_plotting.py b/tests/test_plotting.py index 940639465..ba602dd40 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -331,7 +331,13 @@ def test_generate_profit_graph(testdatadir): trades = trades[trades['pair'].isin(pairs)] - fig = generate_profit_graph(pairs, data, trades, timeframe="5m", stake_currency='BTC') + fig = generate_profit_graph( + pairs, + data, + trades, + timeframe="5m", + stake_currency='BTC', + starting_balance=0) assert isinstance(fig, go.Figure) assert fig.layout.title.text == "Freqtrade Profit plot" @@ -340,7 +346,7 @@ def test_generate_profit_graph(testdatadir): assert fig.layout.yaxis3.title.text == "Profit BTC" figure = fig.layout.figure - assert len(figure.data) == 7 + assert len(figure.data) == 8 avgclose = find_trace_in_fig_data(figure.data, "Avg close price") assert isinstance(avgclose, go.Scatter) @@ -355,6 +361,9 @@ def test_generate_profit_graph(testdatadir): underwater = find_trace_in_fig_data(figure.data, "Underwater Plot") assert isinstance(underwater, go.Scatter) + underwater_relative = find_trace_in_fig_data(figure.data, "Underwater Plot (%)") + assert isinstance(underwater_relative, go.Scatter) + for pair in pairs: profit_pair = find_trace_in_fig_data(figure.data, f"Profit {pair}") assert isinstance(profit_pair, go.Scatter) @@ -362,7 +371,7 @@ def test_generate_profit_graph(testdatadir): with pytest.raises(OperationalException, match=r"No trades found.*"): # Pair cannot be empty - so it's an empty dataframe. generate_profit_graph(pairs, data, trades.loc[trades['pair'].isnull()], timeframe="5m", - stake_currency='BTC') + stake_currency='BTC', starting_balance=0) def test_start_plot_dataframe(mocker): @@ -444,6 +453,7 @@ def test_plot_profit(default_conf, mocker, testdatadir): default_conf['datadir'] = testdatadir default_conf['exportfilename'] = testdatadir / 'backtest-result_test_nofile.json' default_conf['pairs'] = ['ETH/BTC', 'LTC/BTC'] + default_conf['available_capital'] = 1000 profit_mock = MagicMock() store_mock = MagicMock() From e8aec967ddb9ab7d33cee93254201c8330e41a92 Mon Sep 17 00:00:00 2001 From: Nicolas Papp Date: Sun, 24 Apr 2022 17:42:52 -0300 Subject: [PATCH 05/18] Update on note --- freqtrade/data/btanalysis.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 6911941e6..5f5ced053 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -446,7 +446,8 @@ def _calc_drawdown_series(profit_results: pd.DataFrame, *, date_col: str, value_ max_balance = starting_balance + max_drawdown_df['high_value'] max_drawdown_df['drawdown_relative'] = ((max_balance - cumulative_balance) / max_balance) else: - # This is not completely accurate + # NOTE: This is not completely accurate, + # but might good enough if starting_balance is not available max_drawdown_df['drawdown_relative'] = ( (max_drawdown_df['high_value'] - max_drawdown_df['cumulative']) / max_drawdown_df['high_value']) From 9bc6bbe472f58bbec82d741ab916d66c52b2978a Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 25 Apr 2022 07:23:16 +0200 Subject: [PATCH 06/18] Improve test for max_drawdown calculations --- freqtrade/plot/plotting.py | 2 +- tests/data/test_btanalysis.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index ed403e09f..a273f5555 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -1,9 +1,9 @@ import logging from pathlib import Path from typing import Any, Dict, List, Optional -from numpy import number import pandas as pd +from numpy import number from freqtrade.configuration import TimeRange from freqtrade.data.btanalysis import (analyze_trade_parallelism, calculate_max_drawdown, diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index f4275edd9..118ea4ca7 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -362,3 +362,35 @@ 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') + + +@pytest.mark.parametrize('values,relative,result,result_rel', [ + ([0.0, -500.0, 500.0, 10000.0, -1000.0], False, 1000.0, 0.090909), + ([0.0, -500.0, 500.0, 10000.0, -1000.0], True, 1000.0, 0.5), + +]) +def test_calculate_max_drawdown_abs(values, relative, result, result_rel): + """ + Test case from issue https://github.com/freqtrade/freqtrade/issues/6655 + [1000, 500, 1000, 11000, 10000] # absolute results + [1000, 50%, 0%, 0%, ~9%] # Relative drawdowns + """ + + dates = [Arrow(2020, 1, 1).shift(days=i) for i in range(len(values))] + df = DataFrame(zip(values, dates), columns=['profit_abs', 'open_date']) + # sort by profit and reset index + df = df.sort_values('profit_abs').reset_index(drop=True) + df1 = df.copy() + drawdown, hdate, ldate, hval, lval, drawdown_rel = calculate_max_drawdown( + df, date_col='open_date', starting_balance=1000, relative=relative) + # Ensure df has not been altered. + assert df.equals(df1) + + assert isinstance(drawdown, float) + assert isinstance(drawdown_rel, float) + # High must be before low + assert hdate < ldate + # High value must be higher than low value + assert hval > lval + assert drawdown == result + assert pytest.approx(drawdown_rel) == result_rel From 5ff2261b7486df6d4a66bde3dec8dbed5a8c30a9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 25 Apr 2022 07:32:32 +0200 Subject: [PATCH 07/18] Improve test to explicitly test for dates --- tests/data/test_btanalysis.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index 118ea4ca7..2ffd57a54 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -364,20 +364,20 @@ def test_calculate_max_drawdown2(): calculate_max_drawdown(df, date_col='open_date', value_col='profit') -@pytest.mark.parametrize('values,relative,result,result_rel', [ - ([0.0, -500.0, 500.0, 10000.0, -1000.0], False, 1000.0, 0.090909), - ([0.0, -500.0, 500.0, 10000.0, -1000.0], True, 1000.0, 0.5), +@pytest.mark.parametrize('profits,relative,highd,lowd,result,result_rel', [ + ([0.0, -500.0, 500.0, 10000.0, -1000.0], False, 3, 4, 1000.0, 0.090909), + ([0.0, -500.0, 500.0, 10000.0, -1000.0], True, 0, 1, 1000.0, 0.5), ]) -def test_calculate_max_drawdown_abs(values, relative, result, result_rel): +def test_calculate_max_drawdown_abs(profits, relative, highd, lowd, result, result_rel): """ Test case from issue https://github.com/freqtrade/freqtrade/issues/6655 [1000, 500, 1000, 11000, 10000] # absolute results [1000, 50%, 0%, 0%, ~9%] # Relative drawdowns """ - - dates = [Arrow(2020, 1, 1).shift(days=i) for i in range(len(values))] - df = DataFrame(zip(values, dates), columns=['profit_abs', 'open_date']) + init_date = Arrow(2020, 1, 1) + dates = [init_date.shift(days=i) for i in range(len(profits))] + df = DataFrame(zip(profits, dates), columns=['profit_abs', 'open_date']) # sort by profit and reset index df = df.sort_values('profit_abs').reset_index(drop=True) df1 = df.copy() @@ -388,6 +388,9 @@ def test_calculate_max_drawdown_abs(values, relative, result, result_rel): assert isinstance(drawdown, float) assert isinstance(drawdown_rel, float) + assert hdate == init_date.shift(days=highd) + assert ldate == init_date.shift(days=lowd) + # High must be before low assert hdate < ldate # High value must be higher than low value From 4444259078c92ae1ff8adab487828d478bf0fcba Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 25 Apr 2022 11:33:18 +0200 Subject: [PATCH 08/18] Fix hyperopt-loss interface to enforce kwargs --- freqtrade/optimize/hyperopt_loss_interface.py | 4 ++-- freqtrade/plot/plotting.py | 7 +++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/freqtrade/optimize/hyperopt_loss_interface.py b/freqtrade/optimize/hyperopt_loss_interface.py index ac8239b75..8366dcc4f 100644 --- a/freqtrade/optimize/hyperopt_loss_interface.py +++ b/freqtrade/optimize/hyperopt_loss_interface.py @@ -19,11 +19,11 @@ class IHyperOptLoss(ABC): @staticmethod @abstractmethod - def hyperopt_loss_function(results: DataFrame, trade_count: int, + def hyperopt_loss_function(*, results: DataFrame, trade_count: int, min_date: datetime, max_date: datetime, config: Dict, processed: Dict[str, DataFrame], backtest_stats: Dict[str, Any], - *args, **kwargs) -> float: + **kwargs) -> float: """ Objective function, returns smaller number for better results """ diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index a273f5555..0edfd9caf 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -3,7 +3,6 @@ from pathlib import Path from typing import Any, Dict, List, Optional import pandas as pd -from numpy import number from freqtrade.configuration import TimeRange from freqtrade.data.btanalysis import (analyze_trade_parallelism, calculate_max_drawdown, @@ -159,7 +158,7 @@ def add_profit(fig, row, data: pd.DataFrame, column: str, name: str) -> make_sub def add_max_drawdown(fig, row, trades: pd.DataFrame, df_comb: pd.DataFrame, - timeframe: str, starting_balance: number) -> make_subplots: + timeframe: str, starting_balance: float) -> make_subplots: """ Add scatter points indicating max drawdown """ @@ -192,7 +191,7 @@ def add_max_drawdown(fig, row, trades: pd.DataFrame, df_comb: pd.DataFrame, return fig -def add_underwater(fig, row, trades: pd.DataFrame, starting_balance: number) -> make_subplots: +def add_underwater(fig, row, trades: pd.DataFrame, starting_balance: float) -> make_subplots: """ Add underwater plots """ @@ -526,7 +525,7 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra def generate_profit_graph(pairs: str, data: Dict[str, pd.DataFrame], trades: pd.DataFrame, timeframe: str, stake_currency: str, - starting_balance: number) -> go.Figure: + starting_balance: float) -> go.Figure: # Combine close-values for all pairs, rename columns to "pair" try: df_comb = combine_dataframes_with_mean(data, "close") From bc5048e4f389ec535375c56f533cd81c24e6c948 Mon Sep 17 00:00:00 2001 From: Nicolas Papp Date: Mon, 25 Apr 2022 23:50:47 -0300 Subject: [PATCH 09/18] Update to backtesting.md --- docs/backtesting.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index 5d836d01b..bc98e81bf 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -439,7 +439,9 @@ It contains some useful key metrics about performance of your strategy on backte - `Rejected Entry signals`: Trade entry signals that could not be acted upon due to `max_open_trades` being reached. - `Entry/Exit Timeouts`: Entry/exit orders which did not fill (only applicable if custom pricing is used). - `Min balance` / `Max balance`: Lowest and Highest Wallet balance during the backtest period. -- `Drawdown (Account)`: Maximum Account Drawdown experienced. Calculated as $(Absolute Drawdown) / (DrawdownHigh + startingBalance)$. +- `Max % of account underwater`: Maximum percentage your account has decreased from the top since the simulation started. +Calculated as the maximum of `(Max Balance - Current Balance) / (Max Balance)`. +- `Absolute Drawdown (Account)`: Maximum Account Drawdown experienced. Calculated as `(Absolute Drawdown) / (DrawdownHigh + startingBalance)`. - `Drawdown`: Maximum, absolute drawdown experienced. Difference between Drawdown High and Subsequent Low point. - `Drawdown high` / `Drawdown low`: Profit at the beginning and end of the largest drawdown period. A negative low value means initial capital lost. - `Drawdown Start` / `Drawdown End`: Start and end datetime for this largest drawdown (can also be visualized via the `plot-dataframe` sub-command). From f9244aad92928d941507ccba73bcf184b5942e6c Mon Sep 17 00:00:00 2001 From: Nicolas Papp Date: Sun, 1 May 2022 12:25:53 -0300 Subject: [PATCH 10/18] Fix on max drawdown formula to match tests --- docs/hyperopt.md | 2 +- freqtrade/data/metrics.py | 2 +- tests/data/test_btanalysis.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index bab062fad..030d73f4b 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -566,7 +566,7 @@ Currently, the following loss functions are builtin: * `SortinoHyperOptLoss` - optimizes Sortino Ratio calculated on trade returns relative to **downside** standard deviation. * `SortinoHyperOptLossDaily` - optimizes Sortino Ratio calculated on **daily** trade returns relative to **downside** standard deviation. * `MaxDrawDownHyperOptLoss` - Optimizes Maximum absolute drawdown. -* `MaxDrawDownRelativeHyperOptLoss` - Similar as the above, but also optimizes Maximum relative drawdown. +* `MaxDrawDownRelativeHyperOptLoss` - Optimizes both maximum absolute drawdown while also adjusting for maximum relative drawdown. * `CalmarHyperOptLoss` - Optimizes Calmar Ratio calculated on trade returns relative to max drawdown. * `ProfitDrawDownHyperOptLoss` - Optimizes by max Profit & min Drawdown objective. `DRAWDOWN_MULT` variable within the hyperoptloss file can be adjusted to be stricter or more flexible on drawdown purposes. diff --git a/freqtrade/data/metrics.py b/freqtrade/data/metrics.py index 5e93ae0dc..79d192f83 100644 --- a/freqtrade/data/metrics.py +++ b/freqtrade/data/metrics.py @@ -153,7 +153,7 @@ def calculate_max_drawdown(trades: pd.DataFrame, *, date_col: str = 'close_date' max_drawdown_rel = max_drawdown_df.loc[idxmin, 'drawdown_relative'] return ( - abs(min(max_drawdown_df['drawdown'])), + abs(max_drawdown_df.loc[idxmin, 'drawdown']), high_date, low_date, high_val, diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index 2cfc33b6b..4157bd899 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -380,7 +380,7 @@ def test_calculate_max_drawdown2(): @pytest.mark.parametrize('profits,relative,highd,lowd,result,result_rel', [ ([0.0, -500.0, 500.0, 10000.0, -1000.0], False, 3, 4, 1000.0, 0.090909), - ([0.0, -500.0, 500.0, 10000.0, -1000.0], True, 0, 1, 1000.0, 0.5), + ([0.0, -500.0, 500.0, 10000.0, -1000.0], True, 0, 1, 500.0, 0.5), ]) def test_calculate_max_drawdown_abs(profits, relative, highd, lowd, result, result_rel): From 7160f9085a8a40dba180b0f4098d6d13e0e65fc0 Mon Sep 17 00:00:00 2001 From: Nicolas Papp Date: Sun, 1 May 2022 12:32:12 -0300 Subject: [PATCH 11/18] Update summary examples --- docs/backtesting.md | 85 ++++++++++++++++++++++----------------------- 1 file changed, 41 insertions(+), 44 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index d925e0e4e..3b1f940fb 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -377,50 +377,47 @@ The last element of the backtest report is the summary metrics table. It contains some useful key metrics about performance of your strategy on backtesting data. ``` -================ SUMMARY METRICS =============== -| Metric | Value | -|------------------------+---------------------| -| Backtesting from | 2019-01-01 00:00:00 | -| Backtesting to | 2019-05-01 00:00:00 | -| Max open trades | 3 | -| | | -| Total/Daily Avg Trades | 429 / 3.575 | -| Starting balance | 0.01000000 BTC | -| Final balance | 0.01762792 BTC | -| Absolute profit | 0.00762792 BTC | -| Total profit % | 76.2% | -| CAGR % | 460.87% | -| Avg. stake amount | 0.001 BTC | -| Total trade volume | 0.429 BTC | -| | | -| Long / Short | 352 / 77 | -| Total profit Long % | 1250.58% | -| Total profit Short % | -15.02% | -| Absolute profit Long | 0.00838792 BTC | -| Absolute profit Short | -0.00076 BTC | -| | | -| Best Pair | LSK/BTC 26.26% | -| Worst Pair | ZEC/BTC -10.18% | -| Best Trade | LSK/BTC 4.25% | -| Worst Trade | ZEC/BTC -10.25% | -| Best day | 0.00076 BTC | -| Worst day | -0.00036 BTC | -| Days win/draw/lose | 12 / 82 / 25 | -| Avg. Duration Winners | 4:23:00 | -| Avg. Duration Loser | 6:55:00 | -| Rejected Entry signals | 3089 | -| Entry/Exit Timeouts | 0 / 0 | -| | | -| Min balance | 0.00945123 BTC | -| Max balance | 0.01846651 BTC | -| Drawdown (Account) | 13.33% | -| Drawdown | 0.0015 BTC | -| Drawdown high | 0.0013 BTC | -| Drawdown low | -0.0002 BTC | -| Drawdown Start | 2019-02-15 14:10:00 | -| Drawdown End | 2019-04-11 18:15:00 | -| Market change | -5.88% | -================================================ +================== SUMMARY METRICS ================== +| Metric | Value | +|-----------------------------+---------------------| +| Backtesting from | 2022-02-01 00:00:00 | +| Backtesting to | 2022-03-15 00:15:00 | +| Max open trades | 10 | +| | | +| Total/Daily Avg Trades | 77 / 1.83 | +| Starting balance | 1000 USDT | +| Final balance | 1135.843 USDT | +| Absolute profit | 135.843 USDT | +| Total profit % | 13.58% | +| CAGR % | 202.51% | +| Trades per day | 1.83 | +| Avg. daily profit % | 0.32% | +| Avg. stake amount | 105.996 USDT | +| Total trade volume | 8161.695 USDT | +| | | +| Best Pair | THETA/USDT 61.28% | +| Worst Pair | SAND/USDT -15.57% | +| Best trade | THETA/USDT 25.47% | +| Worst trade | SAND/USDT -5.19% | +| Best day | 73.347 USDT | +| Worst day | -56.261 USDT | +| Days win/draw/lose | 12 / 9 / 11 | +| Avg. Duration Winners | 1 day, 19:30:00 | +| Avg. Duration Loser | 20:31:00 | +| Rejected Entry signals | 16959 | +| Entry/Exit Timeouts | 0 / 0 | +| | | +| Min balance | 970.12 USDT | +| Max balance | 1141.775 USDT | +| Max % of account underwater | 7.07% | +| Absolute Drawdown (Account) | 7.07% | +| Absolute Drawdown | 77.666 USDT | +| Drawdown high | 97.995 USDT | +| Drawdown low | 20.329 USDT | +| Drawdown Start | 2022-02-11 08:00:00 | +| Drawdown End | 2022-02-13 15:30:00 | +| Market change | -6.67% | +===================================================== ``` From 1e2523af612c4dae3613f8f8df860d0a5a849c78 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 2 May 2022 19:44:14 +0200 Subject: [PATCH 12/18] Fix some assumptions on the data available_capital is not guaranteed to be available, while dry-run-wallet is. --- freqtrade/data/metrics.py | 6 +++--- .../hyperopt_loss/hyperopt_loss_max_drawdown_relative.py | 2 +- freqtrade/plot/plotting.py | 3 ++- tests/test_plotting.py | 1 - 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/freqtrade/data/metrics.py b/freqtrade/data/metrics.py index 79d192f83..c11a2df88 100644 --- a/freqtrade/data/metrics.py +++ b/freqtrade/data/metrics.py @@ -1,5 +1,5 @@ import logging -from typing import Dict, Optional, Tuple +from typing import Dict, Tuple import numpy as np import pandas as pd @@ -73,7 +73,7 @@ def create_cum_profit(df: pd.DataFrame, trades: pd.DataFrame, col_name: str, def _calc_drawdown_series(profit_results: pd.DataFrame, *, date_col: str, value_col: str, - starting_balance: Optional[float] = 0.0) -> pd.DataFrame: + starting_balance: float) -> 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() @@ -93,7 +93,7 @@ def _calc_drawdown_series(profit_results: pd.DataFrame, *, date_col: str, value_ def calculate_underwater(trades: pd.DataFrame, *, date_col: str = 'close_date', - value_col: str = 'profit_ratio', starting_balance: Optional[float] = 0.0 + value_col: str = 'profit_ratio', starting_balance: float = 0.0 ): """ Calculate max drawdown and the corresponding close dates diff --git a/freqtrade/optimize/hyperopt_loss/hyperopt_loss_max_drawdown_relative.py b/freqtrade/optimize/hyperopt_loss/hyperopt_loss_max_drawdown_relative.py index 393aaa2c8..3182afb47 100644 --- a/freqtrade/optimize/hyperopt_loss/hyperopt_loss_max_drawdown_relative.py +++ b/freqtrade/optimize/hyperopt_loss/hyperopt_loss_max_drawdown_relative.py @@ -36,7 +36,7 @@ class MaxDrawDownRelativeHyperOptLoss(IHyperOptLoss): drawdown_df = calculate_underwater( results, value_col='profit_abs', - starting_balance=config['available_capital'] + starting_balance=config['dry_run_wallet'] ) max_drawdown = abs(min(drawdown_df['drawdown'])) relative_drawdown = max(drawdown_df['drawdown_relative']) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 3a4eaf4f4..37758d05f 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -692,7 +692,8 @@ def plot_profit(config: Dict[str, Any]) -> None: # this could be useful to gauge the overall market trend fig = generate_profit_graph(plot_elements['pairs'], plot_elements['ohlcv'], trades, config['timeframe'], - config.get('stake_currency', ''), config['available_capital']) + config.get('stake_currency', ''), + config.get('available_capital', config['dry_run_wallet'])) store_plot_file(fig, filename='freqtrade-profit-plot.html', directory=config['user_data_dir'] / 'plot', auto_open=config.get('plot_auto_open', False)) diff --git a/tests/test_plotting.py b/tests/test_plotting.py index 630007352..9ee7a75c6 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -454,7 +454,6 @@ def test_plot_profit(default_conf, mocker, testdatadir): default_conf['datadir'] = testdatadir default_conf['exportfilename'] = testdatadir / 'backtest-result_test_nofile.json' default_conf['pairs'] = ['ETH/BTC', 'LTC/BTC'] - default_conf['available_capital'] = 1000 profit_mock = MagicMock() store_mock = MagicMock() From 3f64c6307fe92a96d5a08f8e535b125960c98c8a Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 2 May 2022 20:01:44 +0200 Subject: [PATCH 13/18] Maintain compatibility with old backtest results --- freqtrade/optimize/optimize_reports.py | 34 ++++++++++++++++---------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index e65fd6498..42db366a1 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -733,6 +733,26 @@ def text_table_add_metrics(strat_results: Dict) -> str: strat_results['stake_currency'])), ] if strat_results.get('trade_count_short', 0) > 0 else [] + drawdown_metrics = [] + if 'max_relative_drawdown' in strat_results: + # Compatibility to show old hyperopt results + drawdown_metrics.append( + ('Max % of account underwater', f"{strat_results['max_relative_drawdown']:.2%}") + ) + drawdown_metrics.extend([ + ('Absolute Drawdown (Account)', f"{strat_results['max_drawdown_account']:.2%}") + if 'max_drawdown_account' in strat_results else ( + 'Drawdown', f"{strat_results['max_drawdown']:.2%}"), + ('Absolute Drawdown', round_coin_value(strat_results['max_drawdown_abs'], + strat_results['stake_currency'])), + ('Drawdown high', round_coin_value(strat_results['max_drawdown_high'], + strat_results['stake_currency'])), + ('Drawdown low', round_coin_value(strat_results['max_drawdown_low'], + strat_results['stake_currency'])), + ('Drawdown Start', strat_results['drawdown_start']), + ('Drawdown End', strat_results['drawdown_end']), + ]) + # Newly added fields should be ignored if they are missing in strat_results. hyperopt-show # command stores these results and newer version of freqtrade must be able to handle old # results with missing new fields. @@ -788,19 +808,7 @@ def text_table_add_metrics(strat_results: Dict) -> str: ('Max balance', round_coin_value(strat_results['csum_max'], strat_results['stake_currency'])), - # Compatibility to show old hyperopt results - ('Max % of account underwater', f"{strat_results['max_relative_drawdown']:.2%}"), - ('Absolute Drawdown (Account)', f"{strat_results['max_drawdown_account']:.2%}") - if 'max_drawdown_account' in strat_results else ( - 'Drawdown', f"{strat_results['max_drawdown']:.2%}"), - ('Absolute Drawdown', round_coin_value(strat_results['max_drawdown_abs'], - strat_results['stake_currency'])), - ('Drawdown high', round_coin_value(strat_results['max_drawdown_high'], - strat_results['stake_currency'])), - ('Drawdown low', round_coin_value(strat_results['max_drawdown_low'], - strat_results['stake_currency'])), - ('Drawdown Start', strat_results['drawdown_start']), - ('Drawdown End', strat_results['drawdown_end']), + *drawdown_metrics, ('Market change', f"{strat_results['market_change']:.2%}"), ] From 7a5762991884af58ba3367299409d65469a27344 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 2 May 2022 20:08:38 +0200 Subject: [PATCH 14/18] Keep Backtest-metrics aligned --- docs/backtesting.md | 154 +++++++++++++++++++++++--------------------- 1 file changed, 82 insertions(+), 72 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index 3b1f940fb..75225b654 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -287,45 +287,51 @@ A backtesting result will look like that: | ADA/BTC | 1 | 0.89 | 0.89 | 0.00004434 | 0.44 | 6:00:00 | 1 0 0 100 | | LTC/BTC | 1 | 0.68 | 0.68 | 0.00003421 | 0.34 | 2:00:00 | 1 0 0 100 | | TOTAL | 2 | 0.78 | 1.57 | 0.00007855 | 0.78 | 4:00:00 | 2 0 0 100 | -================ SUMMARY METRICS =============== -| Metric | Value | -|------------------------+---------------------| -| Backtesting from | 2019-01-01 00:00:00 | -| Backtesting to | 2019-05-01 00:00:00 | -| Max open trades | 3 | -| | | -| Total/Daily Avg Trades | 429 / 3.575 | -| Starting balance | 0.01000000 BTC | -| Final balance | 0.01762792 BTC | -| Absolute profit | 0.00762792 BTC | -| Total profit % | 76.2% | -| CAGR % | 460.87% | -| Trades per day | 3.575 | -| Avg. stake amount | 0.001 BTC | -| Total trade volume | 0.429 BTC | -| | | -| Best Pair | LSK/BTC 26.26% | -| Worst Pair | ZEC/BTC -10.18% | -| Best Trade | LSK/BTC 4.25% | -| Worst Trade | ZEC/BTC -10.25% | -| Best day | 0.00076 BTC | -| Worst day | -0.00036 BTC | -| Days win/draw/lose | 12 / 82 / 25 | -| Avg. Duration Winners | 4:23:00 | -| Avg. Duration Loser | 6:55:00 | -| Rejected Entry signals | 3089 | -| Entry/Exit Timeouts | 0 / 0 | -| | | -| Min balance | 0.00945123 BTC | -| Max balance | 0.01846651 BTC | -| Drawdown (Account) | 13.33% | -| Drawdown | 0.0015 BTC | -| Drawdown high | 0.0013 BTC | -| Drawdown low | -0.0002 BTC | -| Drawdown Start | 2019-02-15 14:10:00 | -| Drawdown End | 2019-04-11 18:15:00 | -| Market change | -5.88% | -=============================================== +================== SUMMARY METRICS ================== +| Metric | Value | +|-----------------------------+---------------------| +| Backtesting from | 2019-01-01 00:00:00 | +| Backtesting to | 2019-05-01 00:00:00 | +| Max open trades | 3 | +| | | +| Total/Daily Avg Trades | 429 / 3.575 | +| Starting balance | 0.01000000 BTC | +| Final balance | 0.01762792 BTC | +| Absolute profit | 0.00762792 BTC | +| Total profit % | 76.2% | +| CAGR % | 460.87% | +| Avg. stake amount | 0.001 BTC | +| Total trade volume | 0.429 BTC | +| | | +| Long / Short | 352 / 77 | +| Total profit Long % | 1250.58% | +| Total profit Short % | -15.02% | +| Absolute profit Long | 0.00838792 BTC | +| Absolute profit Short | -0.00076 BTC | +| | | +| Best Pair | LSK/BTC 26.26% | +| Worst Pair | ZEC/BTC -10.18% | +| Best Trade | LSK/BTC 4.25% | +| Worst Trade | ZEC/BTC -10.25% | +| Best day | 0.00076 BTC | +| Worst day | -0.00036 BTC | +| Days win/draw/lose | 12 / 82 / 25 | +| Avg. Duration Winners | 4:23:00 | +| Avg. Duration Loser | 6:55:00 | +| Rejected Entry signals | 3089 | +| Entry/Exit Timeouts | 0 / 0 | +| | | +| Min balance | 0.00945123 BTC | +| Max balance | 0.01846651 BTC | +| Max % of account underwater | 25.19% | +| Absolute Drawdown (Account) | 13.33% | +| Drawdown | 0.0015 BTC | +| Drawdown high | 0.0013 BTC | +| Drawdown low | -0.0002 BTC | +| Drawdown Start | 2019-02-15 14:10:00 | +| Drawdown End | 2019-04-11 18:15:00 | +| Market change | -5.88% | +===================================================== ``` ### Backtesting report table @@ -380,43 +386,47 @@ It contains some useful key metrics about performance of your strategy on backte ================== SUMMARY METRICS ================== | Metric | Value | |-----------------------------+---------------------| -| Backtesting from | 2022-02-01 00:00:00 | -| Backtesting to | 2022-03-15 00:15:00 | -| Max open trades | 10 | +| Backtesting from | 2019-01-01 00:00:00 | +| Backtesting to | 2019-05-01 00:00:00 | +| Max open trades | 3 | | | | -| Total/Daily Avg Trades | 77 / 1.83 | -| Starting balance | 1000 USDT | -| Final balance | 1135.843 USDT | -| Absolute profit | 135.843 USDT | -| Total profit % | 13.58% | -| CAGR % | 202.51% | -| Trades per day | 1.83 | -| Avg. daily profit % | 0.32% | -| Avg. stake amount | 105.996 USDT | -| Total trade volume | 8161.695 USDT | +| Total/Daily Avg Trades | 429 / 3.575 | +| Starting balance | 0.01000000 BTC | +| Final balance | 0.01762792 BTC | +| Absolute profit | 0.00762792 BTC | +| Total profit % | 76.2% | +| CAGR % | 460.87% | +| Avg. stake amount | 0.001 BTC | +| Total trade volume | 0.429 BTC | | | | -| Best Pair | THETA/USDT 61.28% | -| Worst Pair | SAND/USDT -15.57% | -| Best trade | THETA/USDT 25.47% | -| Worst trade | SAND/USDT -5.19% | -| Best day | 73.347 USDT | -| Worst day | -56.261 USDT | -| Days win/draw/lose | 12 / 9 / 11 | -| Avg. Duration Winners | 1 day, 19:30:00 | -| Avg. Duration Loser | 20:31:00 | -| Rejected Entry signals | 16959 | +| Long / Short | 352 / 77 | +| Total profit Long % | 1250.58% | +| Total profit Short % | -15.02% | +| Absolute profit Long | 0.00838792 BTC | +| Absolute profit Short | -0.00076 BTC | +| | | +| Best Pair | LSK/BTC 26.26% | +| Worst Pair | ZEC/BTC -10.18% | +| Best Trade | LSK/BTC 4.25% | +| Worst Trade | ZEC/BTC -10.25% | +| Best day | 0.00076 BTC | +| Worst day | -0.00036 BTC | +| Days win/draw/lose | 12 / 82 / 25 | +| Avg. Duration Winners | 4:23:00 | +| Avg. Duration Loser | 6:55:00 | +| Rejected Entry signals | 3089 | | Entry/Exit Timeouts | 0 / 0 | | | | -| Min balance | 970.12 USDT | -| Max balance | 1141.775 USDT | -| Max % of account underwater | 7.07% | -| Absolute Drawdown (Account) | 7.07% | -| Absolute Drawdown | 77.666 USDT | -| Drawdown high | 97.995 USDT | -| Drawdown low | 20.329 USDT | -| Drawdown Start | 2022-02-11 08:00:00 | -| Drawdown End | 2022-02-13 15:30:00 | -| Market change | -6.67% | +| Min balance | 0.00945123 BTC | +| Max balance | 0.01846651 BTC | +| Max % of account underwater | 25.19% | +| Absolute Drawdown (Account) | 13.33% | +| Drawdown | 0.0015 BTC | +| Drawdown high | 0.0013 BTC | +| Drawdown low | -0.0002 BTC | +| Drawdown Start | 2019-02-15 14:10:00 | +| Drawdown End | 2019-04-11 18:15:00 | +| Market change | -5.88% | ===================================================== ``` From 8e1cdb9103789f425e93d57faf1b16b6f00e6d23 Mon Sep 17 00:00:00 2001 From: talentoscope <22162766+talentoscope@users.noreply.github.com> Date: Mon, 2 May 2022 23:20:13 +0100 Subject: [PATCH 15/18] Update setup.sh Added curl to dependencies for Debian systems --- setup.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.sh b/setup.sh index e0b010387..dcf6c02c7 100755 --- a/setup.sh +++ b/setup.sh @@ -155,7 +155,7 @@ function install_macos() { # Install bot Debian_ubuntu function install_debian() { sudo apt-get update - sudo apt-get install -y gcc build-essential autoconf libtool pkg-config make wget git $(echo lib${PYTHON}-dev ${PYTHON}-venv) + sudo apt-get install -y gcc build-essential autoconf libtool pkg-config make wget git curl $(echo lib${PYTHON}-dev ${PYTHON}-venv) install_talib } From eb996a152a099e8424c0459da18daa6178a51579 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 3 May 2022 18:03:45 +0200 Subject: [PATCH 16/18] Fix fee handling for futures trades --- freqtrade/exchange/exchange.py | 7 +++++-- tests/exchange/test_exchange.py | 24 ++++++++++++++++++------ 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 2ed10ee7a..08bdab265 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1613,7 +1613,9 @@ class Exchange: order['fee']['cost'] / safe_value_fallback2(order, order, 'filled', 'amount'), 8) elif fee_curr in self.get_pair_quote_currency(order['symbol']): # Quote currency - divide by cost - return round(order['fee']['cost'] / order['cost'], 8) if order['cost'] else None + return round(self._contracts_to_amount( + order['symbol'], order['fee']['cost']) / order['cost'], + 8) if order['cost'] else None else: # If Fee currency is a different currency if not order['cost']: @@ -1628,7 +1630,8 @@ class Exchange: fee_to_quote_rate = self._config['exchange'].get('unknown_fee_rate', None) if not fee_to_quote_rate: return None - return round((order['fee']['cost'] * fee_to_quote_rate) / order['cost'], 8) + return round((self._contracts_to_amount( + order['symbol'], order['fee']['cost']) * fee_to_quote_rate) / order['cost'], 8) def extract_cost_curr_rate(self, order: Dict) -> Tuple[float, str, Optional[float]]: """ diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 31311cc38..1368bcb85 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -4165,7 +4165,10 @@ def test__order_contracts_to_amount( 'cost': 60.0, 'filled': None, 'remaining': 30.0, - 'fee': 0.06, + 'fee': { + 'currency': 'USDT', + 'cost': 0.06, + }, 'fees': [{ 'currency': 'USDT', 'cost': 0.06, @@ -4192,7 +4195,10 @@ def test__order_contracts_to_amount( 'cost': 80.0, 'filled': None, 'remaining': 40.0, - 'fee': 0.08, + 'fee': { + 'currency': 'USDT', + 'cost': 0.08, + }, 'fees': [{ 'currency': 'USDT', 'cost': 0.08, @@ -4226,12 +4232,18 @@ def test__order_contracts_to_amount( 'info': {}, }, ] + order1_bef = orders[0] + order2_bef = orders[1] + order1 = exchange._order_contracts_to_amount(deepcopy(order1_bef)) + order2 = exchange._order_contracts_to_amount(deepcopy(order2_bef)) + assert order1['amount'] == order1_bef['amount'] * contract_size + assert order1['cost'] == order1_bef['cost'] * contract_size - order1 = exchange._order_contracts_to_amount(orders[0]) - order2 = exchange._order_contracts_to_amount(orders[1]) + assert order2['amount'] == order2_bef['amount'] * contract_size + assert order2['cost'] == order2_bef['cost'] * contract_size + + # Don't fail exchange._order_contracts_to_amount(orders[2]) - assert order1['amount'] == 30.0 * contract_size - assert order2['amount'] == 40.0 * contract_size @pytest.mark.parametrize('pair,contract_size,trading_mode', [ From 091cb4fb8d4b1f775deabb06d52f8fa2f0850a1f Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 3 May 2022 19:42:17 +0200 Subject: [PATCH 17/18] Reduce no stake amount verbosity closes #6768 --- freqtrade/freqtradebot.py | 1 - 1 file changed, 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 025746553..c52ce1b1c 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -602,7 +602,6 @@ class FreqtradeBot(LoggingMixin): pair, price, stake_amount, trade_side, enter_tag, trade) if not stake_amount: - logger.info(f"No stake amount to enter a trade for {pair}.") return False if pos_adjust: From ce035a59478d730aec8dd0e7d58a5baf68128a91 Mon Sep 17 00:00:00 2001 From: Mark Regan Date: Tue, 3 May 2022 23:34:12 +0100 Subject: [PATCH 18/18] Add bot_loop_start() call in plotting.py plotting.py was missing a call to strategy.bot_loop_start() resulting in strategies using this callback to not work. Made changes and confirmed plotting now works for strategies using bot_loop_start() callback. LMK if anything else needed for PR. --- freqtrade/plot/plotting.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 37758d05f..ce8f54cbd 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -634,6 +634,7 @@ def load_and_plot_trades(config: Dict[str, Any]): exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config) IStrategy.dp = DataProvider(config, exchange) strategy.bot_start() + strategy.bot_loop_start() plot_elements = init_plotscript(config, list(exchange.markets), strategy.startup_candle_count) timerange = plot_elements['timerange'] trades = plot_elements['trades']