From 16a516a882f8936df3e4191e0311cedbc9d7a120 Mon Sep 17 00:00:00 2001 From: Italo Date: Wed, 19 Jan 2022 01:50:15 +0000 Subject: [PATCH 001/118] added plot functionality --- freqtrade/optimize/hyperopt.py | 65 +++++++++++++++++++++++++++++++++- 1 file changed, 64 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 209edd157..cfbc3ea82 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -32,6 +32,11 @@ from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss # noqa: F4 from freqtrade.optimize.hyperopt_tools import HyperoptTools, hyperopt_serializer from freqtrade.optimize.optimize_reports import generate_strategy_stats from freqtrade.resolvers.hyperopt_resolver import HyperOptLossResolver +from skopt.plots import plot_convergence, plot_regret, plot_evaluations, plot_objective +import matplotlib.pyplot as plt +import numpy as np +import random +from sklearn.base import clone # Suppress scikit-learn FutureWarnings from skopt @@ -476,7 +481,12 @@ class Hyperopt: asked = self.opt.ask(n_points=current_jobs) f_val = self.run_optimizer_parallel(parallel, asked, i) - self.opt.tell(asked, [v['loss'] for v in f_val]) + res = self.opt.tell(asked, [v['loss'] for v in f_val]) + + self.plot_optimizer(res, path='user_data/scripts', convergence=False, regret=False, mse=True, objective=True, jobs=jobs) + + if res.models and hasattr(res.models[-1], "kernel_"): + print(f'kernel: {res.models[-1].kernel_}') # Calculate progressbar outputs for j, val in enumerate(f_val): @@ -521,3 +531,56 @@ class Hyperopt: # This is printed when Ctrl+C is pressed quickly, before first epochs have # a chance to be evaluated. print("No epochs evaluated yet, no best result.") + + def plot_mse(self, res, ax, jobs): + if len(res.x_iters) < 10: + return + + if not hasattr(self, 'mse_list'): + self.mse_list = [] + + model = clone(res.models[-1]) + i_subset = random.sample(range(len(res.x_iters)), 100) if len(res.x_iters) > 100 else range(len(res.x_iters)) + + i_train = random.sample(i_subset, round(.8*len(i_subset))) # get 80% random indices + x_train = [x for i, x in enumerate(res.x_iters) if i in i_train] + y_train = [y for i, y in enumerate(res.func_vals) if i in i_train] + + i_test = [i for i in i_subset if i not in i_train] # get 20% random indices + x_test = [x for i, x in enumerate(res.x_iters) if i in i_test] + y_test = [y for i, y in enumerate(res.func_vals) if i in i_test] + model.fit(np.array(x_train), np.array(y_train)) + y_pred, sigma = model.predict(np.array(x_test), return_std=True) + mse = np.mean((y_test - y_pred) ** 2) + self.mse_list.append(mse) + + ax.plot(range(INITIAL_POINTS, INITIAL_POINTS + jobs * len(self.mse_list), jobs), self.mse_list, label='MSE', marker=".", markersize=12, lw=2) + + def plot_optimizer(self, res, path, jobs, convergence=True, regret=True, evaluations=True, objective=True, mse=True): + path = Path(path) + if convergence: + ax = plot_convergence(res) + ax.flatten()[0].figure.savefig(path / 'convergence.png') + + if regret: + ax = plot_regret(res) + ax.flatten()[0].figure.savefig(path / 'regret.png') + + if evaluations: +# print('evaluations') + ax = plot_evaluations(res) + ax.flatten()[0].figure.savefig(path / 'evaluations.png') + + if objective and res.models: +# print('objective') + ax = plot_objective(res, sample_source='result', n_samples=50, n_points=10) + ax.flatten()[0].figure.savefig(path / 'objective.png') + + if mse and res.models: +# print('mse') + fig, ax = plt.subplots() + ax.set_ylabel('MSE') + ax.set_xlabel('Epoch') + ax.set_title('MSE') + ax = self.plot_mse(res, ax, jobs) + fig.savefig(path / 'mse.png') From 2eec51bfcbdc27279b21390cc5fa3ee7069233f3 Mon Sep 17 00:00:00 2001 From: Italo Date: Wed, 19 Jan 2022 02:00:14 +0000 Subject: [PATCH 002/118] Update requirements-hyperopt.txt --- requirements-hyperopt.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements-hyperopt.txt b/requirements-hyperopt.txt index 122243bf2..57bb25e2c 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -8,3 +8,4 @@ scikit-optimize==0.9.0 filelock==3.4.2 joblib==1.1.0 progressbar2==4.0.0 +matplotlib \ No newline at end of file From 52206e6f41926ef89f7d9c051c377de9fc16f7ff Mon Sep 17 00:00:00 2001 From: Italo <45588475+italodamato@users.noreply.github.com> Date: Thu, 20 Jan 2022 17:15:05 +0000 Subject: [PATCH 003/118] add buy tag to plot --- freqtrade/plot/plotting.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 3769d4c5a..b8a747105 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -236,6 +236,7 @@ def plot_trades(fig, trades: pd.DataFrame) -> make_subplots: if trades is not None and len(trades) > 0: # Create description for sell summarizing the trade trades['desc'] = trades.apply(lambda row: f"{row['profit_ratio']:.2%}, " + f"{row['buy_tag']}, " f"{row['sell_reason']}, " f"{row['trade_duration']} min", axis=1) From 0ce6c150ff6e1dfdba0a3a7bdda97288fd77eaa0 Mon Sep 17 00:00:00 2001 From: Italo <45588475+italodamato@users.noreply.github.com> Date: Sat, 22 Jan 2022 14:06:45 +0000 Subject: [PATCH 004/118] set stoploss at trade creation --- freqtrade/optimize/backtesting.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index ae4001f5f..9cfeedd75 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -521,6 +521,7 @@ class Backtesting: exchange='backtesting', orders=[] ) + trade.adjust_stop_loss(trade.open_rate, self.strategy.stoploss, initial=True) order = Order( ft_is_open=False, From a2fb241a3b210f100db025815fb3542410476b45 Mon Sep 17 00:00:00 2001 From: Italo <45588475+italodamato@users.noreply.github.com> Date: Mon, 24 Jan 2022 01:35:42 +0000 Subject: [PATCH 005/118] increase initial points to 64 --- freqtrade/optimize/hyperopt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index cfbc3ea82..f49f3f307 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -50,7 +50,7 @@ progressbar.streams.wrap_stdout() logger = logging.getLogger(__name__) -INITIAL_POINTS = 30 +INITIAL_POINTS = 64 # Keep no more than SKOPT_MODEL_QUEUE_SIZE models # in the skopt model queue, to optimize memory consumption From 992eac9efaf8c7bdc98abd0c350ccb8930555cd0 Mon Sep 17 00:00:00 2001 From: Italo <45588475+italodamato@users.noreply.github.com> Date: Sat, 5 Feb 2022 17:36:19 +0000 Subject: [PATCH 006/118] Update hyperopt.py --- freqtrade/optimize/hyperopt.py | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 60c54fe40..5e59135bd 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -50,7 +50,7 @@ progressbar.streams.wrap_stdout() logger = logging.getLogger(__name__) -INITIAL_POINTS = 64 +INITIAL_POINTS = 32 # Keep no more than SKOPT_MODEL_QUEUE_SIZE models # in the skopt model queue, to optimize memory consumption @@ -532,7 +532,8 @@ class Hyperopt: # a chance to be evaluated. print("No epochs evaluated yet, no best result.") - def plot_mse(self, res, ax, jobs): + def plot_mse(self, res, ax, jobs): + from sklearn.model_selection import cross_val_score if len(res.x_iters) < 10: return @@ -540,19 +541,26 @@ class Hyperopt: self.mse_list = [] model = clone(res.models[-1]) - i_subset = random.sample(range(len(res.x_iters)), 100) if len(res.x_iters) > 100 else range(len(res.x_iters)) + # i_subset = random.sample(range(len(res.x_iters)), 100) if len(res.x_iters) > 100 else range(len(res.x_iters)) - i_train = random.sample(i_subset, round(.8*len(i_subset))) # get 80% random indices - x_train = [x for i, x in enumerate(res.x_iters) if i in i_train] - y_train = [y for i, y in enumerate(res.func_vals) if i in i_train] + # i_train = random.sample(i_subset, round(.8*len(i_subset))) # get 80% random indices + # x_train = [x for i, x in enumerate(res.x_iters) if i in i_train] + # y_train = [y for i, y in enumerate(res.func_vals) if i in i_train] - i_test = [i for i in i_subset if i not in i_train] # get 20% random indices - x_test = [x for i, x in enumerate(res.x_iters) if i in i_test] - y_test = [y for i, y in enumerate(res.func_vals) if i in i_test] - model.fit(np.array(x_train), np.array(y_train)) - y_pred, sigma = model.predict(np.array(x_test), return_std=True) - mse = np.mean((y_test - y_pred) ** 2) - self.mse_list.append(mse) + # i_test = [i for i in i_subset if i not in i_train] # get 20% random indices + # x_test = [x for i, x in enumerate(res.x_iters) if i in i_test] + # y_test = [y for i, y in enumerate(res.func_vals) if i in i_test] + model.fit(res.x_iters, res.func_vals) + # Perform a cross-validation estimate of the coefficient of determination using + # the cross_validation module using all CPUs available on the machine + # K = 5 # folds + R2 = cross_val_score(model, X=res.x_iters, y=res.func_vals, cv=5, n_jobs=jobs).mean() + print(f'R2: {R2}') + R2 = R2 if R2 > -5 else -5 + self.mse_list.append(R2) + # y_pred, sigma = model.predict(np.array(x_test), return_std=True) + # mse = np.mean((y_test - y_pred) ** 2) + # self.mse_list.append(mse) ax.plot(range(INITIAL_POINTS, INITIAL_POINTS + jobs * len(self.mse_list), jobs), self.mse_list, label='MSE', marker=".", markersize=12, lw=2) From 6a4cae1f8c2f7569dff9156431e36dd1162810b9 Mon Sep 17 00:00:00 2001 From: Italo <45588475+italodamato@users.noreply.github.com> Date: Sun, 6 Feb 2022 00:17:48 +0000 Subject: [PATCH 007/118] Update hyperopt.py --- freqtrade/optimize/hyperopt.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 5e59135bd..25055d06c 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -540,7 +540,7 @@ class Hyperopt: if not hasattr(self, 'mse_list'): self.mse_list = [] - model = clone(res.models[-1]) + # model = clone(res.models[-1]) # i_subset = random.sample(range(len(res.x_iters)), 100) if len(res.x_iters) > 100 else range(len(res.x_iters)) # i_train = random.sample(i_subset, round(.8*len(i_subset))) # get 80% random indices @@ -550,11 +550,11 @@ class Hyperopt: # i_test = [i for i in i_subset if i not in i_train] # get 20% random indices # x_test = [x for i, x in enumerate(res.x_iters) if i in i_test] # y_test = [y for i, y in enumerate(res.func_vals) if i in i_test] - model.fit(res.x_iters, res.func_vals) + # model.fit(res.x_iters, res.func_vals) # Perform a cross-validation estimate of the coefficient of determination using # the cross_validation module using all CPUs available on the machine # K = 5 # folds - R2 = cross_val_score(model, X=res.x_iters, y=res.func_vals, cv=5, n_jobs=jobs).mean() + R2 = cross_val_score(res.models[-1], X=res.x_iters, y=res.func_vals, cv=5, n_jobs=jobs).mean() print(f'R2: {R2}') R2 = R2 if R2 > -5 else -5 self.mse_list.append(R2) From 6c1729e20b6ea87d341ad8b2e204409c7ded5be6 Mon Sep 17 00:00:00 2001 From: Italo <45588475+italodamato@users.noreply.github.com> Date: Sun, 6 Feb 2022 01:07:30 +0000 Subject: [PATCH 008/118] ignore warnings --- freqtrade/optimize/hyperopt.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 25055d06c..d99c2b3b0 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -554,7 +554,9 @@ class Hyperopt: # Perform a cross-validation estimate of the coefficient of determination using # the cross_validation module using all CPUs available on the machine # K = 5 # folds - R2 = cross_val_score(res.models[-1], X=res.x_iters, y=res.func_vals, cv=5, n_jobs=jobs).mean() + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + R2 = cross_val_score(res.models[-1], X=res.x_iters, y=res.func_vals, cv=5, n_jobs=jobs).mean() print(f'R2: {R2}') R2 = R2 if R2 > -5 else -5 self.mse_list.append(R2) From adf8f6b2d503649effaed161e81108df10483a76 Mon Sep 17 00:00:00 2001 From: Italo <45588475+italodamato@users.noreply.github.com> Date: Sun, 6 Feb 2022 10:33:49 +0000 Subject: [PATCH 009/118] Update hyperopt.py --- freqtrade/optimize/hyperopt.py | 56 +++++++++++----------------------- 1 file changed, 17 insertions(+), 39 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index d99c2b3b0..19132a14a 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -32,18 +32,17 @@ from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss # noqa: F4 from freqtrade.optimize.hyperopt_tools import HyperoptTools, hyperopt_serializer from freqtrade.optimize.optimize_reports import generate_strategy_stats from freqtrade.resolvers.hyperopt_resolver import HyperOptLossResolver -from skopt.plots import plot_convergence, plot_regret, plot_evaluations, plot_objective import matplotlib.pyplot as plt import numpy as np import random -from sklearn.base import clone - # Suppress scikit-learn FutureWarnings from skopt with warnings.catch_warnings(): warnings.filterwarnings("ignore", category=FutureWarning) from skopt import Optimizer from skopt.space import Dimension + from sklearn.model_selection import cross_val_score + from skopt.plots import plot_convergence, plot_regret, plot_evaluations, plot_objective progressbar.streams.wrap_stderr() progressbar.streams.wrap_stdout() @@ -483,7 +482,7 @@ class Hyperopt: f_val = self.run_optimizer_parallel(parallel, asked, i) res = self.opt.tell(asked, [v['loss'] for v in f_val]) - self.plot_optimizer(res, path='user_data/scripts', convergence=False, regret=False, mse=True, objective=True, jobs=jobs) + self.plot_optimizer(res, path='user_data/scripts', convergence=False, regret=False, r2=True, objective=True, jobs=jobs) if res.models and hasattr(res.models[-1], "kernel_"): print(f'kernel: {res.models[-1].kernel_}') @@ -532,41 +531,21 @@ class Hyperopt: # a chance to be evaluated. print("No epochs evaluated yet, no best result.") - def plot_mse(self, res, ax, jobs): - from sklearn.model_selection import cross_val_score + def plot_r2(self, res, ax, jobs): if len(res.x_iters) < 10: return - if not hasattr(self, 'mse_list'): - self.mse_list = [] + if not hasattr(self, 'r2_list'): + self.r2_list = [] - # model = clone(res.models[-1]) - # i_subset = random.sample(range(len(res.x_iters)), 100) if len(res.x_iters) > 100 else range(len(res.x_iters)) - - # i_train = random.sample(i_subset, round(.8*len(i_subset))) # get 80% random indices - # x_train = [x for i, x in enumerate(res.x_iters) if i in i_train] - # y_train = [y for i, y in enumerate(res.func_vals) if i in i_train] - - # i_test = [i for i in i_subset if i not in i_train] # get 20% random indices - # x_test = [x for i, x in enumerate(res.x_iters) if i in i_test] - # y_test = [y for i, y in enumerate(res.func_vals) if i in i_test] - # model.fit(res.x_iters, res.func_vals) - # Perform a cross-validation estimate of the coefficient of determination using - # the cross_validation module using all CPUs available on the machine - # K = 5 # folds - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - R2 = cross_val_score(res.models[-1], X=res.x_iters, y=res.func_vals, cv=5, n_jobs=jobs).mean() - print(f'R2: {R2}') - R2 = R2 if R2 > -5 else -5 - self.mse_list.append(R2) - # y_pred, sigma = model.predict(np.array(x_test), return_std=True) - # mse = np.mean((y_test - y_pred) ** 2) - # self.mse_list.append(mse) + r2 = cross_val_score(res.models[-1], X=res.x_iters, y=res.func_vals, scoring='r2', cv=5, n_jobs=jobs).mean() + print(f'R2: {r2}') + r2 = r2 if r2 > -5 else -5 + self.r2_list.append(r2) - ax.plot(range(INITIAL_POINTS, INITIAL_POINTS + jobs * len(self.mse_list), jobs), self.mse_list, label='MSE', marker=".", markersize=12, lw=2) + ax.plot(range(INITIAL_POINTS, INITIAL_POINTS + jobs * len(self.r2_list), jobs), self.r2_list, label='R2', marker=".", markersize=12, lw=2) - def plot_optimizer(self, res, path, jobs, convergence=True, regret=True, evaluations=True, objective=True, mse=True): + def plot_optimizer(self, res, path, jobs, convergence=True, regret=True, evaluations=True, objective=True, r2=True): path = Path(path) if convergence: ax = plot_convergence(res) @@ -586,11 +565,10 @@ class Hyperopt: ax = plot_objective(res, sample_source='result', n_samples=50, n_points=10) ax.flatten()[0].figure.savefig(path / 'objective.png') - if mse and res.models: -# print('mse') + if r2 and res.models: fig, ax = plt.subplots() - ax.set_ylabel('MSE') + ax.set_ylabel('R2') ax.set_xlabel('Epoch') - ax.set_title('MSE') - ax = self.plot_mse(res, ax, jobs) - fig.savefig(path / 'mse.png') + ax.set_title('R2') + ax = self.plot_r2(res, ax, jobs) + fig.savefig(path / 'r2.png') From d03378b1df7c76b7d1931c174af9197abf977357 Mon Sep 17 00:00:00 2001 From: Italo <45588475+italodamato@users.noreply.github.com> Date: Sun, 6 Feb 2022 15:32:59 +0000 Subject: [PATCH 010/118] Update hyperopt.py --- freqtrade/optimize/hyperopt.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 19132a14a..ba32943cb 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -538,7 +538,10 @@ class Hyperopt: if not hasattr(self, 'r2_list'): self.r2_list = [] - r2 = cross_val_score(res.models[-1], X=res.x_iters, y=res.func_vals, scoring='r2', cv=5, n_jobs=jobs).mean() + model = res.models[-1] + model.criterion = 'squared_error' + + r2 = cross_val_score(model, X=res.x_iters, y=res.func_vals, scoring='r2', cv=5, n_jobs=jobs).mean() print(f'R2: {r2}') r2 = r2 if r2 > -5 else -5 self.r2_list.append(r2) From d2a54483050a587facad827afa6d3177cd68d702 Mon Sep 17 00:00:00 2001 From: Italo <45588475+italodamato@users.noreply.github.com> Date: Fri, 11 Mar 2022 17:38:32 +0000 Subject: [PATCH 011/118] Update hyperopt.py --- freqtrade/optimize/hyperopt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index ba32943cb..aa255967e 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -486,6 +486,7 @@ class Hyperopt: if res.models and hasattr(res.models[-1], "kernel_"): print(f'kernel: {res.models[-1].kernel_}') + print(datetime.now()) # Calculate progressbar outputs for j, val in enumerate(f_val): @@ -542,7 +543,6 @@ class Hyperopt: model.criterion = 'squared_error' r2 = cross_val_score(model, X=res.x_iters, y=res.func_vals, scoring='r2', cv=5, n_jobs=jobs).mean() - print(f'R2: {r2}') r2 = r2 if r2 > -5 else -5 self.r2_list.append(r2) From 162e94455b3cf928ac68bd895a7674f1dbe0ce8b Mon Sep 17 00:00:00 2001 From: froggleston Date: Wed, 16 Mar 2022 12:16:24 +0000 Subject: [PATCH 012/118] Add support for storing buy candle indicator rows in backtesting results --- freqtrade/optimize/backtesting.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 79c861ee8..1a8c0903c 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -64,7 +64,8 @@ class Backtesting: config['dry_run'] = True self.strategylist: List[IStrategy] = [] self.all_results: Dict[str, Dict] = {} - + self.processed_dfs: Dict[str, Dict] = {} + self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config) self.dataprovider = DataProvider(self.config, None) @@ -136,6 +137,10 @@ class Backtesting: self.config['startup_candle_count'] = self.required_startup self.exchange.validate_required_startup_candles(self.required_startup, self.timeframe) + self.enable_backtest_signal_candle_export = False + if self.config.get('enable_backtest_signal_candle_export', None) is not None: + self.enable_backtest_signal_candle_export = bool(self.config.get('enable_backtest_signal_candle_export')) + self.progress = BTProgress() self.abort = False @@ -636,6 +641,25 @@ class Backtesting: }) self.all_results[self.strategy.get_strategy_name()] = results + if self.enable_backtest_signal_candle_export: + signal_candles_only = {} + for pair in preprocessed_tmp.keys(): + signal_candles_only_df = DataFrame() + + pairdf = preprocessed_tmp[pair] + resdf = results['results'] + pairresults = resdf.loc[(resdf["pair"] == pair)] + + if pairdf.shape[0] > 0: + for t, v in pairresults.open_date.items(): + allinds = pairdf.loc[(pairdf['date'] < v)] + signal_inds = allinds.iloc[[-1]] + signal_candles_only_df = signal_candles_only_df.append(signal_inds) + + signal_candles_only[pair] = signal_candles_only_df + + self.processed_dfs[self.strategy.get_strategy_name()] = signal_candles_only + return min_date, max_date def start(self) -> None: From d796ce09352042160023235a404899d2973a55c7 Mon Sep 17 00:00:00 2001 From: Italo <45588475+italodamato@users.noreply.github.com> Date: Sun, 20 Mar 2022 15:41:14 +0000 Subject: [PATCH 013/118] Update hyperopt.py 1. Try to get points using `self.opt.ask` first 2. Discard the points that have already been evaluated 3. Retry using `self.opt.ask` up to 3 times 4. If still some points are missing in respect to `n_points`, random sample some points 5. Repeat until at least `n_points` points in the `asked_non_tried` list 6. Return a list with legth truncated at `n_points` --- freqtrade/optimize/hyperopt.py | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index aa255967e..badaf2b3c 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -413,6 +413,31 @@ class Hyperopt: f'({(self.max_date - self.min_date).days} days)..') # Store non-trimmed data - will be trimmed after signal generation. dump(preprocessed, self.data_pickle_file) + + def get_asked_points(self, n_points: int) -> List[Any]: + ''' + Steps: + 1. Try to get points using `self.opt.ask` first + 2. Discard the points that have already been evaluated + 3. Retry using `self.opt.ask` up to 3 times + 4. If still some points are missing in respect to `n_points`, random sample some points + 5. Repeat until at least `n_points` points in the `asked_non_tried` list + 6. Return a list with legth truncated at `n_points` + ''' + i = 0 + asked_non_tried = [] + while i < 100: + if len(asked_non_tried) < n_points: + if i < 3: + asked = self.opt.ask(n_points=n_points) + else: + # use random sample if `self.opt.ask` returns points points already tried + asked = self.opt.space.rvs(n_samples=n_points * 5) + asked_non_tried += [x for x in asked if x not in self.opt.Xi and x not in asked_non_tried] + i += 1 + else: + break + return asked_non_tried[:n_points] def start(self) -> None: self.random_state = self._set_random_state(self.config.get('hyperopt_random_state', None)) @@ -478,11 +503,11 @@ class Hyperopt: n_rest = (i + 1) * jobs - self.total_epochs current_jobs = jobs - n_rest if n_rest > 0 else jobs - asked = self.opt.ask(n_points=current_jobs) + asked = self.get_asked_points(n_points=current_jobs) f_val = self.run_optimizer_parallel(parallel, asked, i) res = self.opt.tell(asked, [v['loss'] for v in f_val]) - self.plot_optimizer(res, path='user_data/scripts', convergence=False, regret=False, r2=True, objective=True, jobs=jobs) + self.plot_optimizer(res, path='user_data/scripts', convergence=False, regret=False, r2=False, objective=True, jobs=jobs) if res.models and hasattr(res.models[-1], "kernel_"): print(f'kernel: {res.models[-1].kernel_}') From e16bb1b34e381b9bb82b47d9e406f093479c5ffa Mon Sep 17 00:00:00 2001 From: Italo <45588475+italodamato@users.noreply.github.com> Date: Sun, 20 Mar 2022 16:02:03 +0000 Subject: [PATCH 014/118] Optimize only new points Enforce points returned from `self.opt.ask` have not been already evaluated --- freqtrade/optimize/hyperopt.py | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 9664e6f07..8b6225fa7 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -410,6 +410,35 @@ class Hyperopt: # Store non-trimmed data - will be trimmed after signal generation. dump(preprocessed, self.data_pickle_file) + def get_asked_points(self, n_points: int) -> List[List[Any]]: + ''' + Enforce points returned from `self.opt.ask` have not been already evaluated + + Steps: + 1. Try to get points using `self.opt.ask` first + 2. Discard the points that have already been evaluated + 3. Retry using `self.opt.ask` up to 3 times + 4. If still some points are missing in respect to `n_points`, random sample some points + 5. Repeat until at least `n_points` points in the `asked_non_tried` list + 6. Return a list with legth truncated at `n_points` + ''' + i = 0 + asked_non_tried: List[List[Any]] = [] + while i < 100: + if len(asked_non_tried) < n_points: + if i < 3: + asked = self.opt.ask(n_points=n_points) + else: + # use random sample if `self.opt.ask` returns points points already tried + asked = self.opt.space.rvs(n_samples=n_points * 5) + asked_non_tried += [x for x in asked + if x not in self.opt.Xi + and x not in asked_non_tried] + i += 1 + else: + break + return asked_non_tried[:n_points] + def start(self) -> None: self.random_state = self._set_random_state(self.config.get('hyperopt_random_state', None)) logger.info(f"Using optimizer random state: {self.random_state}") @@ -474,7 +503,7 @@ class Hyperopt: n_rest = (i + 1) * jobs - self.total_epochs current_jobs = jobs - n_rest if n_rest > 0 else jobs - asked = self.opt.ask(n_points=current_jobs) + asked = self.get_asked_points(n_points=current_jobs) f_val = self.run_optimizer_parallel(parallel, asked, i) self.opt.tell(asked, [v['loss'] for v in f_val]) From 0fd269e4f00feefe431a16f2fd45f4fd113dc5e7 Mon Sep 17 00:00:00 2001 From: Italo <45588475+italodamato@users.noreply.github.com> Date: Sun, 20 Mar 2022 16:03:07 +0000 Subject: [PATCH 015/118] typo --- freqtrade/optimize/hyperopt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 8b6225fa7..55ae44b91 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -420,7 +420,7 @@ class Hyperopt: 3. Retry using `self.opt.ask` up to 3 times 4. If still some points are missing in respect to `n_points`, random sample some points 5. Repeat until at least `n_points` points in the `asked_non_tried` list - 6. Return a list with legth truncated at `n_points` + 6. Return a list with length truncated at `n_points` ''' i = 0 asked_non_tried: List[List[Any]] = [] From 23f1a1904bfa90d2fa00865e71023f869479adfe Mon Sep 17 00:00:00 2001 From: Italo <45588475+italodamato@users.noreply.github.com> Date: Sun, 20 Mar 2022 16:06:41 +0000 Subject: [PATCH 016/118] more compact --- freqtrade/optimize/hyperopt.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 55ae44b91..61e8913df 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -424,19 +424,15 @@ class Hyperopt: ''' i = 0 asked_non_tried: List[List[Any]] = [] - while i < 100: - if len(asked_non_tried) < n_points: - if i < 3: - asked = self.opt.ask(n_points=n_points) - else: - # use random sample if `self.opt.ask` returns points points already tried - asked = self.opt.space.rvs(n_samples=n_points * 5) - asked_non_tried += [x for x in asked - if x not in self.opt.Xi - and x not in asked_non_tried] - i += 1 + while i < 100 and len(asked_non_tried) < n_points: + if i < 3: + asked = self.opt.ask(n_points=n_points) else: - break + asked = self.opt.space.rvs(n_samples=n_points * 5) + asked_non_tried += [x for x in asked + if x not in self.opt.Xi + and x not in asked_non_tried] + i += 1 return asked_non_tried[:n_points] def start(self) -> None: From f8a674f24de013853e4b5acee075fc23b23bd64b Mon Sep 17 00:00:00 2001 From: Italo <45588475+italodamato@users.noreply.github.com> Date: Sun, 20 Mar 2022 16:08:38 +0000 Subject: [PATCH 017/118] make robust in case all points have been tried --- freqtrade/optimize/hyperopt.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 61e8913df..c1adbf45e 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -433,7 +433,10 @@ class Hyperopt: if x not in self.opt.Xi and x not in asked_non_tried] i += 1 - return asked_non_tried[:n_points] + if asked_non_tried: + return asked_non_tried[:n_points] + else: + return self.opt.ask(n_points=n_points) def start(self) -> None: self.random_state = self._set_random_state(self.config.get('hyperopt_random_state', None)) From fca93d8dfec22dad88496774c8482d07d25ca325 Mon Sep 17 00:00:00 2001 From: Italo <45588475+italodamato@users.noreply.github.com> Date: Sun, 20 Mar 2022 16:12:06 +0000 Subject: [PATCH 018/118] Update hyperopt.py --- freqtrade/optimize/hyperopt.py | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index badaf2b3c..bbdc8bf27 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -414,30 +414,33 @@ class Hyperopt: # Store non-trimmed data - will be trimmed after signal generation. dump(preprocessed, self.data_pickle_file) - def get_asked_points(self, n_points: int) -> List[Any]: + def get_asked_points(self, n_points: int) -> List[List[Any]]: ''' + Enforce points returned from `self.opt.ask` have not been already evaluated + Steps: 1. Try to get points using `self.opt.ask` first 2. Discard the points that have already been evaluated 3. Retry using `self.opt.ask` up to 3 times 4. If still some points are missing in respect to `n_points`, random sample some points 5. Repeat until at least `n_points` points in the `asked_non_tried` list - 6. Return a list with legth truncated at `n_points` + 6. Return a list with length truncated at `n_points` ''' i = 0 - asked_non_tried = [] - while i < 100: - if len(asked_non_tried) < n_points: - if i < 3: - asked = self.opt.ask(n_points=n_points) - else: - # use random sample if `self.opt.ask` returns points points already tried - asked = self.opt.space.rvs(n_samples=n_points * 5) - asked_non_tried += [x for x in asked if x not in self.opt.Xi and x not in asked_non_tried] - i += 1 + asked_non_tried: List[List[Any]] = [] + while i < 100 and len(asked_non_tried) < n_points: + if i < 3: + asked = self.opt.ask(n_points=n_points) else: - break - return asked_non_tried[:n_points] + asked = self.opt.space.rvs(n_samples=n_points * 5) + asked_non_tried += [x for x in asked + if x not in self.opt.Xi + and x not in asked_non_tried] + i += 1 + if asked_non_tried: + return asked_non_tried[:n_points] + else: + return self.opt.ask(n_points=n_points) def start(self) -> None: self.random_state = self._set_random_state(self.config.get('hyperopt_random_state', None)) From 37a43019d6427952bfabd17b838590155fde14f1 Mon Sep 17 00:00:00 2001 From: Italo <45588475+italodamato@users.noreply.github.com> Date: Mon, 21 Mar 2022 11:36:53 +0000 Subject: [PATCH 019/118] fix - clear cache before calling `ask` - avoid errors in case asked_non_tried has less than n_points elements --- freqtrade/optimize/hyperopt.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index c1adbf45e..fe587a702 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -426,6 +426,7 @@ class Hyperopt: asked_non_tried: List[List[Any]] = [] while i < 100 and len(asked_non_tried) < n_points: if i < 3: + self.opt.cache_ = {} asked = self.opt.ask(n_points=n_points) else: asked = self.opt.space.rvs(n_samples=n_points * 5) @@ -434,7 +435,7 @@ class Hyperopt: and x not in asked_non_tried] i += 1 if asked_non_tried: - return asked_non_tried[:n_points] + return asked_non_tried[:min(len(asked_non_tried), n_points)] else: return self.opt.ask(n_points=n_points) From 2733aa33b6661523f721bd039cb8fd9e2ccdc7f1 Mon Sep 17 00:00:00 2001 From: Italo <45588475+italodamato@users.noreply.github.com> Date: Tue, 22 Mar 2022 00:28:11 +0000 Subject: [PATCH 020/118] Update hyperopt.py --- freqtrade/optimize/hyperopt.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index bbdc8bf27..f08fa7233 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -430,6 +430,7 @@ class Hyperopt: asked_non_tried: List[List[Any]] = [] while i < 100 and len(asked_non_tried) < n_points: if i < 3: + self.opt.cache_ = {} asked = self.opt.ask(n_points=n_points) else: asked = self.opt.space.rvs(n_samples=n_points * 5) @@ -438,7 +439,7 @@ class Hyperopt: and x not in asked_non_tried] i += 1 if asked_non_tried: - return asked_non_tried[:n_points] + return asked_non_tried[:min(len(asked_non_tried), n_points)] else: return self.opt.ask(n_points=n_points) From b5a346a46de13e7aefcd6be27ac354e701322b6d Mon Sep 17 00:00:00 2001 From: Italo <45588475+italodamato@users.noreply.github.com> Date: Tue, 22 Mar 2022 11:01:38 +0000 Subject: [PATCH 021/118] Update hyperopt.py --- freqtrade/optimize/hyperopt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index f08fa7233..d3f6a72f2 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -431,7 +431,7 @@ class Hyperopt: while i < 100 and len(asked_non_tried) < n_points: if i < 3: self.opt.cache_ = {} - asked = self.opt.ask(n_points=n_points) + asked = self.opt.ask(n_points=n_points * 5) else: asked = self.opt.space.rvs(n_samples=n_points * 5) asked_non_tried += [x for x in asked From 229b0b037eb3440f9fc93bd5d809f05eb5506ff4 Mon Sep 17 00:00:00 2001 From: Italo <45588475+italodamato@users.noreply.github.com> Date: Tue, 29 Mar 2022 19:33:35 +0100 Subject: [PATCH 022/118] reduce search loops --- freqtrade/optimize/hyperopt.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index fe587a702..4fad76570 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -424,10 +424,10 @@ class Hyperopt: ''' i = 0 asked_non_tried: List[List[Any]] = [] - while i < 100 and len(asked_non_tried) < n_points: + while i < 5 and len(asked_non_tried) < n_points: if i < 3: self.opt.cache_ = {} - asked = self.opt.ask(n_points=n_points) + asked = self.opt.ask(n_points=n_points * 5) else: asked = self.opt.space.rvs(n_samples=n_points * 5) asked_non_tried += [x for x in asked From a3b401a762bbdda75191956ea251d097c08e7a32 Mon Sep 17 00:00:00 2001 From: Italo <45588475+italodamato@users.noreply.github.com> Date: Wed, 30 Mar 2022 00:29:14 +0100 Subject: [PATCH 023/118] highlight random points in hyperopt results table --- freqtrade/optimize/hyperopt.py | 21 ++++++++++++++++----- freqtrade/optimize/hyperopt_tools.py | 12 +++++++----- tests/conftest.py | 13 ++++++++++++- tests/optimize/test_hyperopt.py | 2 ++ 4 files changed, 37 insertions(+), 11 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 4fad76570..35f382469 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -10,7 +10,7 @@ import warnings from datetime import datetime, timezone from math import ceil from pathlib import Path -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List, Optional, Tuple import progressbar import rapidjson @@ -410,7 +410,7 @@ class Hyperopt: # Store non-trimmed data - will be trimmed after signal generation. dump(preprocessed, self.data_pickle_file) - def get_asked_points(self, n_points: int) -> List[List[Any]]: + def get_asked_points(self, n_points: int) -> Tuple[List[List[Any]], List[bool]]: ''' Enforce points returned from `self.opt.ask` have not been already evaluated @@ -424,20 +424,30 @@ class Hyperopt: ''' i = 0 asked_non_tried: List[List[Any]] = [] + is_random: List[bool] = [] while i < 5 and len(asked_non_tried) < n_points: if i < 3: self.opt.cache_ = {} asked = self.opt.ask(n_points=n_points * 5) + is_random = [False for _ in range(len(asked))] else: asked = self.opt.space.rvs(n_samples=n_points * 5) + is_random = [True for _ in range(len(asked))] asked_non_tried += [x for x in asked if x not in self.opt.Xi and x not in asked_non_tried] + is_random += [rand for x, rand in zip(asked, is_random) + if x not in self.opt.Xi + and x not in asked_non_tried] i += 1 + if asked_non_tried: - return asked_non_tried[:min(len(asked_non_tried), n_points)] + return ( + asked_non_tried[:min(len(asked_non_tried), n_points)], + is_random[:min(len(asked_non_tried), n_points)] + ) else: - return self.opt.ask(n_points=n_points) + return self.opt.ask(n_points=n_points), [False for _ in range(n_points)] def start(self) -> None: self.random_state = self._set_random_state(self.config.get('hyperopt_random_state', None)) @@ -503,7 +513,7 @@ class Hyperopt: n_rest = (i + 1) * jobs - self.total_epochs current_jobs = jobs - n_rest if n_rest > 0 else jobs - asked = self.get_asked_points(n_points=current_jobs) + asked, is_random = self.get_asked_points(n_points=current_jobs) f_val = self.run_optimizer_parallel(parallel, asked, i) self.opt.tell(asked, [v['loss'] for v in f_val]) @@ -522,6 +532,7 @@ class Hyperopt: # evaluations can take different time. Here they are aligned in the # order they will be shown to the user. val['is_best'] = is_best + val['is_random'] = is_random[j] self.print_results(val) if is_best: diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py index 8c84f772a..83df7e83c 100755 --- a/freqtrade/optimize/hyperopt_tools.py +++ b/freqtrade/optimize/hyperopt_tools.py @@ -322,12 +322,12 @@ class HyperoptTools(): 'results_metrics.profit_total', 'results_metrics.holding_avg', 'results_metrics.max_drawdown', 'results_metrics.max_drawdown_account', 'results_metrics.max_drawdown_abs', - 'loss', 'is_initial_point', 'is_best']] + 'loss', 'is_initial_point', 'is_random', 'is_best']] trials.columns = [ 'Best', 'Epoch', 'Trades', ' Win Draw Loss', 'Avg profit', 'Total profit', 'Profit', 'Avg duration', 'max_drawdown', 'max_drawdown_account', - 'max_drawdown_abs', 'Objective', 'is_initial_point', 'is_best' + 'max_drawdown_abs', 'Objective', 'is_initial_point', 'is_random', 'is_best' ] return trials @@ -349,9 +349,11 @@ class HyperoptTools(): trials = HyperoptTools.prepare_trials_columns(trials, has_account_drawdown) trials['is_profit'] = False - trials.loc[trials['is_initial_point'], 'Best'] = '* ' + trials.loc[trials['is_initial_point'] | trials['is_random'], 'Best'] = '* ' trials.loc[trials['is_best'], 'Best'] = 'Best' - trials.loc[trials['is_initial_point'] & trials['is_best'], 'Best'] = '* Best' + trials.loc[ + (trials['is_initial_point'] | trials['is_random']) & trials['is_best'], + 'Best'] = '* Best' trials.loc[trials['Total profit'] > 0, 'is_profit'] = True trials['Trades'] = trials['Trades'].astype(str) # perc_multi = 1 if legacy_mode else 100 @@ -407,7 +409,7 @@ class HyperoptTools(): trials.iat[i, j] = "{}{}{}".format(Style.BRIGHT, str(trials.loc[i][j]), Style.RESET_ALL) - trials = trials.drop(columns=['is_initial_point', 'is_best', 'is_profit']) + trials = trials.drop(columns=['is_initial_point', 'is_best', 'is_profit', 'is_random']) if remove_header > 0: table = tabulate.tabulate( trials.to_dict(orient='list'), tablefmt='orgtbl', diff --git a/tests/conftest.py b/tests/conftest.py index 57122c01c..1dd6e8869 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2053,6 +2053,7 @@ def saved_hyperopt_results(): 'total_profit': -0.00125625, 'current_epoch': 1, 'is_initial_point': True, + 'is_random': False, 'is_best': True, }, { @@ -2069,6 +2070,7 @@ def saved_hyperopt_results(): 'total_profit': 6.185e-05, 'current_epoch': 2, 'is_initial_point': True, + 'is_random': False, 'is_best': False }, { 'loss': 14.241196856510731, @@ -2079,6 +2081,7 @@ def saved_hyperopt_results(): 'total_profit': -0.13639474, 'current_epoch': 3, 'is_initial_point': True, + 'is_random': False, 'is_best': False }, { 'loss': 100000, @@ -2086,7 +2089,7 @@ def saved_hyperopt_results(): 'params_details': {'buy': {'mfi-value': 13, 'fastd-value': 35, 'adx-value': 39, 'rsi-value': 29, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 87, 'sell-fastd-value': 54, 'sell-adx-value': 63, 'sell-rsi-value': 93, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.411946348378729, 215: 0.2052334363683207, 891: 0.06264755784937427, 2293: 0}, 'stoploss': {'stoploss': -0.11818343570194478}}, # noqa: E501 'results_metrics': {'total_trades': 0, 'wins': 0, 'draws': 0, 'losses': 0, 'profit_mean': None, 'profit_median': None, 'profit_total': 0, 'profit': 0.0, 'holding_avg': timedelta()}, # noqa: E501 'results_explanation': ' 0 trades. Avg profit nan%. Total profit 0.00000000 BTC ( 0.00Σ%). Avg duration nan min.', # noqa: E501 - 'total_profit': 0, 'current_epoch': 4, 'is_initial_point': True, 'is_best': False + 'total_profit': 0, 'current_epoch': 4, 'is_initial_point': True, 'is_random': False, 'is_best': False }, { 'loss': 0.22195522184191518, 'params_dict': {'mfi-value': 17, 'fastd-value': 21, 'adx-value': 38, 'rsi-value': 33, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': False, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 87, 'sell-fastd-value': 82, 'sell-adx-value': 78, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 1269, 'roi_t2': 601, 'roi_t3': 444, 'roi_p1': 0.07280999507931168, 'roi_p2': 0.08946698095898986, 'roi_p3': 0.1454876733325284, 'stoploss': -0.18181041180901014}, # noqa: E501 @@ -2096,6 +2099,7 @@ def saved_hyperopt_results(): 'total_profit': -0.002480140000000001, 'current_epoch': 5, 'is_initial_point': True, + 'is_random': False, 'is_best': True }, { 'loss': 0.545315889154162, @@ -2106,6 +2110,7 @@ def saved_hyperopt_results(): 'total_profit': -0.0041773, 'current_epoch': 6, 'is_initial_point': True, + 'is_random': False, 'is_best': False }, { 'loss': 4.713497421432944, @@ -2118,6 +2123,7 @@ def saved_hyperopt_results(): 'total_profit': -0.06339929, 'current_epoch': 7, 'is_initial_point': True, + 'is_random': False, 'is_best': False }, { 'loss': 20.0, # noqa: E501 @@ -2128,6 +2134,7 @@ def saved_hyperopt_results(): 'total_profit': 0.0, 'current_epoch': 8, 'is_initial_point': True, + 'is_random': False, 'is_best': False }, { 'loss': 2.4731817780991223, @@ -2138,6 +2145,7 @@ def saved_hyperopt_results(): 'total_profit': -0.044050070000000004, # noqa: E501 'current_epoch': 9, 'is_initial_point': True, + 'is_random': False, 'is_best': False }, { 'loss': -0.2604606005845212, # noqa: E501 @@ -2148,6 +2156,7 @@ def saved_hyperopt_results(): 'total_profit': 0.00021629, 'current_epoch': 10, 'is_initial_point': True, + 'is_random': False, 'is_best': True }, { 'loss': 4.876465945994304, # noqa: E501 @@ -2159,6 +2168,7 @@ def saved_hyperopt_results(): 'total_profit': -0.07436117, 'current_epoch': 11, 'is_initial_point': True, + 'is_random': False, 'is_best': False }, { 'loss': 100000, @@ -2169,6 +2179,7 @@ def saved_hyperopt_results(): 'total_profit': 0, 'current_epoch': 12, 'is_initial_point': True, + 'is_random': False, 'is_best': False } ] diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index cc551277a..ef32e2466 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -41,6 +41,7 @@ def generate_result_metrics(): 'max_drawdown_abs': 0.001, 'loss': 0.001, 'is_initial_point': 0.001, + 'is_random': False, 'is_best': 1, } @@ -247,6 +248,7 @@ def test_log_results_if_loss_improves(hyperopt, capsys) -> None: 'total_profit': 0, 'current_epoch': 2, # This starts from 1 (in a human-friendly manner) 'is_initial_point': False, + 'is_random': False, 'is_best': True } ) From 9f171193ef2b893b7f7c9269b9a2c6b796f0d71c Mon Sep 17 00:00:00 2001 From: Italo <45588475+italodamato@users.noreply.github.com> Date: Wed, 30 Mar 2022 09:39:07 +0100 Subject: [PATCH 024/118] Revert "Merge branch 'plot_hyperopt_stats' into opt-ask-force-new-points" This reverts commit 4eb9cc6e8b7bc99b543c4acbfa6c2b07f67d54e5, reversing changes made to a3b401a762bbdda75191956ea251d097c08e7a32. --- .github/workflows/ci.yml | 10 +-- .github/workflows/docker_update_readme.yml | 2 +- docs/includes/pricing.md | 8 +- docs/requirements-docs.txt | 7 +- docs/strategy-advanced.md | 17 ++-- freqtrade/__init__.py | 17 +++- freqtrade/optimize/hyperopt.py | 89 +------------------ freqtrade/resolvers/iresolver.py | 60 ++++--------- requirements-dev.txt | 12 +-- requirements-hyperopt.txt | 1 - requirements.txt | 8 +- tests/conftest.py | 30 +++---- tests/exchange/test_exchange.py | 2 +- .../strategy/strats/hyperoptable_strategy.py | 88 +++++++++++++++++- tests/strategy/strats/strategy_test_v2.py | 2 +- 15 files changed, 169 insertions(+), 184 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b8df7ab10..216a53bc1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,14 +31,14 @@ jobs: python-version: ${{ matrix.python-version }} - name: Cache_dependencies - uses: actions/cache@v3 + uses: actions/cache@v2 id: cache with: path: ~/dependencies/ key: ${{ runner.os }}-dependencies - name: pip cache (linux) - uses: actions/cache@v3 + uses: actions/cache@v2 if: runner.os == 'Linux' with: path: ~/.cache/pip @@ -126,14 +126,14 @@ jobs: python-version: ${{ matrix.python-version }} - name: Cache_dependencies - uses: actions/cache@v3 + uses: actions/cache@v2 id: cache with: path: ~/dependencies/ key: ${{ runner.os }}-dependencies - name: pip cache (macOS) - uses: actions/cache@v3 + uses: actions/cache@v2 if: runner.os == 'macOS' with: path: ~/Library/Caches/pip @@ -218,7 +218,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Pip cache (Windows) - uses: actions/cache@v3 + uses: actions/cache@preview with: path: ~\AppData\Local\pip\Cache key: ${{ matrix.os }}-${{ matrix.python-version }}-pip diff --git a/.github/workflows/docker_update_readme.yml b/.github/workflows/docker_update_readme.yml index 822533ee2..ebb773ad7 100644 --- a/.github/workflows/docker_update_readme.yml +++ b/.github/workflows/docker_update_readme.yml @@ -10,7 +10,7 @@ jobs: steps: - uses: actions/checkout@v3 - name: Docker Hub Description - uses: peter-evans/dockerhub-description@v3 + uses: peter-evans/dockerhub-description@v2.4.3 env: DOCKERHUB_USERNAME: ${{ secrets.DOCKER_USERNAME }} DOCKERHUB_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} diff --git a/docs/includes/pricing.md b/docs/includes/pricing.md index 103df6cd3..ed8a45e68 100644 --- a/docs/includes/pricing.md +++ b/docs/includes/pricing.md @@ -51,9 +51,9 @@ When buying with the orderbook enabled (`bid_strategy.use_order_book=True`), Fre #### Buy price without Orderbook enabled -The following section uses `side` as the configured `bid_strategy.price_side` (defaults to `"bid"`). +The following section uses `side` as the configured `bid_strategy.price_side`. -When not using orderbook (`bid_strategy.use_order_book=False`), Freqtrade uses the best `side` price from the ticker if it's below the `last` traded price from the ticker. Otherwise (when the `side` price is above the `last` price), it calculates a rate between `side` and `last` price based on `bid_strategy.ask_last_balance`.. +When not using orderbook (`bid_strategy.use_order_book=False`), Freqtrade uses the best `side` price from the ticker if it's below the `last` traded price from the ticker. Otherwise (when the `side` price is above the `last` price), it calculates a rate between `side` and `last` price. The `bid_strategy.ask_last_balance` configuration parameter controls this. A value of `0.0` will use `side` price, while `1.0` will use the `last` price and values between those interpolate between ask and last price. @@ -88,9 +88,9 @@ When selling with the orderbook enabled (`ask_strategy.use_order_book=True`), Fr #### Sell price without Orderbook enabled -The following section uses `side` as the configured `ask_strategy.price_side` (defaults to `"ask"`). +When not using orderbook (`ask_strategy.use_order_book=False`), the price at the `ask_strategy.price_side` side (defaults to `"ask"`) from the ticker will be used as the sell price. -When not using orderbook (`ask_strategy.use_order_book=False`), Freqtrade uses the best `side` price from the ticker if it's above the `last` traded price from the ticker. Otherwise (when the `side` price is below the `last` price), it calculates a rate between `side` and `last` price based on `ask_strategy.bid_last_balance`. +When not using orderbook (`ask_strategy.use_order_book=False`), Freqtrade uses the best `side` price from the ticker if it's below the `last` traded price from the ticker. Otherwise (when the `side` price is above the `last` price), it calculates a rate between `side` and `last` price. The `ask_strategy.bid_last_balance` configuration parameter controls this. A value of `0.0` will use `side` price, while `1.0` will use the last price and values between those interpolate between `side` and last price. diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index 1f7db75c5..0ca0e4b63 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,5 +1,4 @@ -mkdocs==1.3.0 -mkdocs-material==8.2.8 +mkdocs==1.2.3 +mkdocs-material==8.2.5 mdx_truly_sane_lists==1.2 -pymdown-extensions==9.3 -jinja2==3.1.1 +pymdown-extensions==9.2 diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index b1f154355..3793abacf 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -146,7 +146,7 @@ def version(self) -> str: The strategies can be derived from other strategies. This avoids duplication of your custom strategy code. You can use this technique to override small parts of your main strategy, leaving the rest untouched: -``` python title="user_data/strategies/myawesomestrategy.py" +``` python class MyAwesomeStrategy(IStrategy): ... stoploss = 0.13 @@ -155,10 +155,6 @@ class MyAwesomeStrategy(IStrategy): # should be in any custom strategy... ... -``` - -``` python title="user_data/strategies/MyAwesomeStrategy2.py" -from myawesomestrategy import MyAwesomeStrategy class MyAwesomeStrategy2(MyAwesomeStrategy): # Override something stoploss = 0.08 @@ -167,7 +163,16 @@ class MyAwesomeStrategy2(MyAwesomeStrategy): Both attributes and methods may be overridden, altering behavior of the original strategy in a way you need. -While keeping the subclass in the same file is technically possible, it can lead to some problems with hyperopt parameter files, we therefore recommend to use separate strategy files, and import the parent strategy as shown above. +!!! Note "Parent-strategy in different files" + If you have the parent-strategy in a different file, you'll need to add the following to the top of your "child"-file to ensure proper loading, otherwise freqtrade may not be able to load the parent strategy correctly. + + ``` python + import sys + from pathlib import Path + sys.path.append(str(Path(__file__).parent)) + + from myawesomestrategy import MyAwesomeStrategy + ``` ## Embedding Strategies diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index f8be8f66f..2747efc96 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,14 +1,27 @@ """ Freqtrade bot """ __version__ = 'develop' -if 'dev' in __version__: +if __version__ == 'develop': + try: import subprocess - __version__ = __version__ + '-' + subprocess.check_output( + __version__ = 'develop-' + subprocess.check_output( ['git', 'log', '--format="%h"', '-n 1'], stderr=subprocess.DEVNULL).decode("utf-8").rstrip().strip('"') + # from datetime import datetime + # last_release = subprocess.check_output( + # ['git', 'tag'] + # ).decode('utf-8').split()[-1].split(".") + # # Releases are in the format "2020.1" - we increment the latest version for dev. + # prefix = f"{last_release[0]}.{int(last_release[1]) + 1}" + # dev_version = int(datetime.now().timestamp() // 1000) + # __version__ = f"{prefix}.dev{dev_version}" + + # subprocess.check_output( + # ['git', 'log', '--format="%h"', '-n 1'], + # stderr=subprocess.DEVNULL).decode("utf-8").rstrip().strip('"') except Exception: # pragma: no cover # git not available, ignore try: diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 223673113..35f382469 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -32,24 +32,20 @@ from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss # noqa: F4 from freqtrade.optimize.hyperopt_tools import HyperoptTools, hyperopt_serializer from freqtrade.optimize.optimize_reports import generate_strategy_stats from freqtrade.resolvers.hyperopt_resolver import HyperOptLossResolver -import matplotlib.pyplot as plt -import numpy as np -import random + # Suppress scikit-learn FutureWarnings from skopt with warnings.catch_warnings(): warnings.filterwarnings("ignore", category=FutureWarning) from skopt import Optimizer from skopt.space import Dimension - from sklearn.model_selection import cross_val_score - from skopt.plots import plot_convergence, plot_regret, plot_evaluations, plot_objective progressbar.streams.wrap_stderr() progressbar.streams.wrap_stdout() logger = logging.getLogger(__name__) -INITIAL_POINTS = 32 +INITIAL_POINTS = 30 # Keep no more than SKOPT_MODEL_QUEUE_SIZE models # in the skopt model queue, to optimize memory consumption @@ -413,35 +409,6 @@ class Hyperopt: f'({(self.max_date - self.min_date).days} days)..') # Store non-trimmed data - will be trimmed after signal generation. dump(preprocessed, self.data_pickle_file) - - def get_asked_points(self, n_points: int) -> List[List[Any]]: - ''' - Enforce points returned from `self.opt.ask` have not been already evaluated - - Steps: - 1. Try to get points using `self.opt.ask` first - 2. Discard the points that have already been evaluated - 3. Retry using `self.opt.ask` up to 3 times - 4. If still some points are missing in respect to `n_points`, random sample some points - 5. Repeat until at least `n_points` points in the `asked_non_tried` list - 6. Return a list with length truncated at `n_points` - ''' - i = 0 - asked_non_tried: List[List[Any]] = [] - while i < 100 and len(asked_non_tried) < n_points: - if i < 3: - self.opt.cache_ = {} - asked = self.opt.ask(n_points=n_points * 5) - else: - asked = self.opt.space.rvs(n_samples=n_points * 5) - asked_non_tried += [x for x in asked - if x not in self.opt.Xi - and x not in asked_non_tried] - i += 1 - if asked_non_tried: - return asked_non_tried[:min(len(asked_non_tried), n_points)] - else: - return self.opt.ask(n_points=n_points) def get_asked_points(self, n_points: int) -> Tuple[List[List[Any]], List[bool]]: ''' @@ -548,13 +515,7 @@ class Hyperopt: asked, is_random = self.get_asked_points(n_points=current_jobs) f_val = self.run_optimizer_parallel(parallel, asked, i) - res = self.opt.tell(asked, [v['loss'] for v in f_val]) - - self.plot_optimizer(res, path='user_data/scripts', convergence=False, regret=False, r2=False, objective=True, jobs=jobs) - - if res.models and hasattr(res.models[-1], "kernel_"): - print(f'kernel: {res.models[-1].kernel_}') - print(datetime.now()) + self.opt.tell(asked, [v['loss'] for v in f_val]) # Calculate progressbar outputs for j, val in enumerate(f_val): @@ -600,47 +561,3 @@ class Hyperopt: # This is printed when Ctrl+C is pressed quickly, before first epochs have # a chance to be evaluated. print("No epochs evaluated yet, no best result.") - - def plot_r2(self, res, ax, jobs): - if len(res.x_iters) < 10: - return - - if not hasattr(self, 'r2_list'): - self.r2_list = [] - - model = res.models[-1] - model.criterion = 'squared_error' - - r2 = cross_val_score(model, X=res.x_iters, y=res.func_vals, scoring='r2', cv=5, n_jobs=jobs).mean() - r2 = r2 if r2 > -5 else -5 - self.r2_list.append(r2) - - ax.plot(range(INITIAL_POINTS, INITIAL_POINTS + jobs * len(self.r2_list), jobs), self.r2_list, label='R2', marker=".", markersize=12, lw=2) - - def plot_optimizer(self, res, path, jobs, convergence=True, regret=True, evaluations=True, objective=True, r2=True): - path = Path(path) - if convergence: - ax = plot_convergence(res) - ax.flatten()[0].figure.savefig(path / 'convergence.png') - - if regret: - ax = plot_regret(res) - ax.flatten()[0].figure.savefig(path / 'regret.png') - - if evaluations: -# print('evaluations') - ax = plot_evaluations(res) - ax.flatten()[0].figure.savefig(path / 'evaluations.png') - - if objective and res.models: -# print('objective') - ax = plot_objective(res, sample_source='result', n_samples=50, n_points=10) - ax.flatten()[0].figure.savefig(path / 'objective.png') - - if r2 and res.models: - fig, ax = plt.subplots() - ax.set_ylabel('R2') - ax.set_xlabel('Epoch') - ax.set_title('R2') - ax = self.plot_r2(res, ax, jobs) - fig.savefig(path / 'r2.png') diff --git a/freqtrade/resolvers/iresolver.py b/freqtrade/resolvers/iresolver.py index 3ab461041..c6f97c976 100644 --- a/freqtrade/resolvers/iresolver.py +++ b/freqtrade/resolvers/iresolver.py @@ -6,7 +6,6 @@ This module load custom objects import importlib.util import inspect import logging -import sys from pathlib import Path from typing import Any, Dict, Iterator, List, Optional, Tuple, Type, Union @@ -16,22 +15,6 @@ from freqtrade.exceptions import OperationalException logger = logging.getLogger(__name__) -class PathModifier: - def __init__(self, path: Path): - self.path = path - - def __enter__(self): - """Inject path to allow importing with relative imports.""" - sys.path.insert(0, str(self.path)) - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - """Undo insertion of local path.""" - str_path = str(self.path) - if str_path in sys.path: - sys.path.remove(str_path) - - class IResolver: """ This class contains all the logic to load custom classes @@ -74,32 +57,27 @@ class IResolver: # Generate spec based on absolute path # Pass object_name as first argument to have logging print a reasonable name. - with PathModifier(module_path.parent): - module_name = module_path.stem or "" - spec = importlib.util.spec_from_file_location(module_name, str(module_path)) - if not spec: + spec = importlib.util.spec_from_file_location(object_name or "", str(module_path)) + if not spec: + return iter([None]) + + module = importlib.util.module_from_spec(spec) + try: + spec.loader.exec_module(module) # type: ignore # importlib does not use typehints + except (ModuleNotFoundError, SyntaxError, ImportError, NameError) as err: + # Catch errors in case a specific module is not installed + logger.warning(f"Could not import {module_path} due to '{err}'") + if enum_failed: return iter([None]) - module = importlib.util.module_from_spec(spec) - try: - spec.loader.exec_module(module) # type: ignore # importlib does not use typehints - except (ModuleNotFoundError, SyntaxError, ImportError, NameError) as err: - # Catch errors in case a specific module is not installed - logger.warning(f"Could not import {module_path} due to '{err}'") - if enum_failed: - return iter([None]) - - valid_objects_gen = ( - (obj, inspect.getsource(module)) for - name, obj in inspect.getmembers( - module, inspect.isclass) if ((object_name is None or object_name == name) - and issubclass(obj, cls.object_type) - and obj is not cls.object_type - and obj.__module__ == module_name - ) - ) - # The __module__ check ensures we only use strategies that are defined in this folder. - return valid_objects_gen + valid_objects_gen = ( + (obj, inspect.getsource(module)) for + name, obj in inspect.getmembers( + module, inspect.isclass) if ((object_name is None or object_name == name) + and issubclass(obj, cls.object_type) + and obj is not cls.object_type) + ) + return valid_objects_gen @classmethod def _search_object(cls, directory: Path, *, object_name: str, add_source: bool = False diff --git a/requirements-dev.txt b/requirements-dev.txt index 063cfaa45..c2f3eae8a 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -6,9 +6,9 @@ coveralls==3.3.1 flake8==4.0.1 flake8-tidy-imports==4.6.0 -mypy==0.942 -pytest==7.1.1 -pytest-asyncio==0.18.3 +mypy==0.940 +pytest==7.1.0 +pytest-asyncio==0.18.2 pytest-cov==3.0.0 pytest-mock==3.7.0 pytest-random-order==1.0.4 @@ -22,8 +22,8 @@ nbconvert==6.4.4 # mypy types types-cachetools==5.0.0 types-filelock==3.2.5 -types-requests==2.27.15 -types-tabulate==0.8.6 +types-requests==2.27.12 +types-tabulate==0.8.5 # Extensions to datetime library -types-python-dateutil==2.8.10 \ No newline at end of file +types-python-dateutil==2.8.9 \ No newline at end of file diff --git a/requirements-hyperopt.txt b/requirements-hyperopt.txt index ad85ac71a..aeb7be035 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -8,4 +8,3 @@ scikit-optimize==0.9.0 filelock==3.6.0 joblib==1.1.0 progressbar2==4.0.0 -matplotlib \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index de05b3f7c..f0f030e78 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,22 +2,22 @@ numpy==1.22.3 pandas==1.4.1 pandas-ta==0.3.14b -ccxt==1.77.36 +ccxt==1.76.5 # Pin cryptography for now due to rust build errors with piwheels -cryptography==36.0.2 +cryptography==36.0.1 aiohttp==3.8.1 SQLAlchemy==1.4.32 python-telegram-bot==13.11 arrow==1.2.2 cachetools==4.2.2 requests==2.27.1 -urllib3==1.26.9 +urllib3==1.26.8 jsonschema==4.4.0 TA-Lib==0.4.24 technical==1.3.0 tabulate==0.8.9 pycoingecko==2.2.0 -jinja2==3.1.1 +jinja2==3.0.3 tables==3.7.0 blosc==1.10.6 diff --git a/tests/conftest.py b/tests/conftest.py index 809342c03..1dd6e8869 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1019,8 +1019,8 @@ def limit_buy_order_open(): 'type': 'limit', 'side': 'buy', 'symbol': 'mocked', - 'timestamp': arrow.utcnow().int_timestamp * 1000, 'datetime': arrow.utcnow().isoformat(), + 'timestamp': arrow.utcnow().int_timestamp, 'price': 0.00001099, 'amount': 90.99181073, 'filled': 0.0, @@ -1046,7 +1046,6 @@ def market_buy_order(): 'type': 'market', 'side': 'buy', 'symbol': 'mocked', - 'timestamp': arrow.utcnow().int_timestamp * 1000, 'datetime': arrow.utcnow().isoformat(), 'price': 0.00004099, 'amount': 91.99181073, @@ -1063,7 +1062,6 @@ def market_sell_order(): 'type': 'market', 'side': 'sell', 'symbol': 'mocked', - 'timestamp': arrow.utcnow().int_timestamp * 1000, 'datetime': arrow.utcnow().isoformat(), 'price': 0.00004173, 'amount': 91.99181073, @@ -1080,8 +1078,7 @@ def limit_buy_order_old(): 'type': 'limit', 'side': 'buy', 'symbol': 'mocked', - 'datetime': arrow.utcnow().shift(minutes=-601).isoformat(), - 'timestamp': arrow.utcnow().shift(minutes=-601).int_timestamp * 1000, + 'datetime': str(arrow.utcnow().shift(minutes=-601).datetime), 'price': 0.00001099, 'amount': 90.99181073, 'filled': 0.0, @@ -1097,7 +1094,6 @@ def limit_sell_order_old(): 'type': 'limit', 'side': 'sell', 'symbol': 'ETH/BTC', - 'timestamp': arrow.utcnow().shift(minutes=-601).int_timestamp * 1000, 'datetime': arrow.utcnow().shift(minutes=-601).isoformat(), 'price': 0.00001099, 'amount': 90.99181073, @@ -1114,7 +1110,6 @@ def limit_buy_order_old_partial(): 'type': 'limit', 'side': 'buy', 'symbol': 'ETH/BTC', - 'timestamp': arrow.utcnow().shift(minutes=-601).int_timestamp * 1000, 'datetime': arrow.utcnow().shift(minutes=-601).isoformat(), 'price': 0.00001099, 'amount': 90.99181073, @@ -1144,7 +1139,7 @@ def limit_buy_order_canceled_empty(request): 'info': {}, 'id': '1234512345', 'clientOrderId': None, - 'timestamp': arrow.utcnow().shift(minutes=-601).int_timestamp * 1000, + 'timestamp': arrow.utcnow().shift(minutes=-601).int_timestamp, 'datetime': arrow.utcnow().shift(minutes=-601).isoformat(), 'lastTradeTimestamp': None, 'symbol': 'LTC/USDT', @@ -1165,7 +1160,7 @@ def limit_buy_order_canceled_empty(request): 'info': {}, 'id': 'AZNPFF-4AC4N-7MKTAT', 'clientOrderId': None, - 'timestamp': arrow.utcnow().shift(minutes=-601).int_timestamp * 1000, + 'timestamp': arrow.utcnow().shift(minutes=-601).int_timestamp, 'datetime': arrow.utcnow().shift(minutes=-601).isoformat(), 'lastTradeTimestamp': None, 'status': 'canceled', @@ -1186,7 +1181,7 @@ def limit_buy_order_canceled_empty(request): 'info': {}, 'id': '1234512345', 'clientOrderId': 'alb1234123', - 'timestamp': arrow.utcnow().shift(minutes=-601).int_timestamp * 1000, + 'timestamp': arrow.utcnow().shift(minutes=-601).int_timestamp, 'datetime': arrow.utcnow().shift(minutes=-601).isoformat(), 'lastTradeTimestamp': None, 'symbol': 'LTC/USDT', @@ -1207,7 +1202,7 @@ def limit_buy_order_canceled_empty(request): 'info': {}, 'id': '1234512345', 'clientOrderId': 'alb1234123', - 'timestamp': arrow.utcnow().shift(minutes=-601).int_timestamp * 1000, + 'timestamp': arrow.utcnow().shift(minutes=-601).int_timestamp, 'datetime': arrow.utcnow().shift(minutes=-601).isoformat(), 'lastTradeTimestamp': None, 'symbol': 'LTC/USDT', @@ -1233,7 +1228,7 @@ def limit_sell_order_open(): 'side': 'sell', 'symbol': 'mocked', 'datetime': arrow.utcnow().isoformat(), - 'timestamp': arrow.utcnow().int_timestamp * 1000, + 'timestamp': arrow.utcnow().int_timestamp, 'price': 0.00001173, 'amount': 90.99181073, 'filled': 0.0, @@ -1399,7 +1394,7 @@ def tickers(): 'BLK/BTC': { 'symbol': 'BLK/BTC', 'timestamp': 1522014806072, - 'datetime': '2018-03-25T21:53:26.072Z', + 'datetime': '2018-03-25T21:53:26.720Z', 'high': 0.007745, 'low': 0.007512, 'bid': 0.007729, @@ -1895,8 +1890,7 @@ def buy_order_fee(): 'type': 'limit', 'side': 'buy', 'symbol': 'mocked', - 'timestamp': arrow.utcnow().shift(minutes=-601).int_timestamp * 1000, - 'datetime': arrow.utcnow().shift(minutes=-601).isoformat(), + 'datetime': str(arrow.utcnow().shift(minutes=-601).datetime), 'price': 0.245441, 'amount': 8.0, 'cost': 1.963528, @@ -2205,7 +2199,7 @@ def limit_buy_order_usdt_open(): 'side': 'buy', 'symbol': 'mocked', 'datetime': arrow.utcnow().isoformat(), - 'timestamp': arrow.utcnow().int_timestamp * 1000, + 'timestamp': arrow.utcnow().int_timestamp, 'price': 2.00, 'amount': 30.0, 'filled': 0.0, @@ -2232,7 +2226,7 @@ def limit_sell_order_usdt_open(): 'side': 'sell', 'symbol': 'mocked', 'datetime': arrow.utcnow().isoformat(), - 'timestamp': arrow.utcnow().int_timestamp * 1000, + 'timestamp': arrow.utcnow().int_timestamp, 'price': 2.20, 'amount': 30.0, 'filled': 0.0, @@ -2257,7 +2251,6 @@ def market_buy_order_usdt(): 'type': 'market', 'side': 'buy', 'symbol': 'mocked', - 'timestamp': arrow.utcnow().int_timestamp * 1000, 'datetime': arrow.utcnow().isoformat(), 'price': 2.00, 'amount': 30.0, @@ -2314,7 +2307,6 @@ def market_sell_order_usdt(): 'type': 'market', 'side': 'sell', 'symbol': 'mocked', - 'timestamp': arrow.utcnow().int_timestamp * 1000, 'datetime': arrow.utcnow().isoformat(), 'price': 2.20, 'amount': 30.0, diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index b76cb23e6..ff8383997 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1098,7 +1098,7 @@ def test_create_order(default_conf, mocker, side, ordertype, rate, marketprice, exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) order = exchange.create_order( - pair='ETH/BTC', ordertype=ordertype, side=side, amount=1, rate=rate) + pair='ETH/BTC', ordertype=ordertype, side=side, amount=1, rate=200) assert 'id' in order assert 'info' in order diff --git a/tests/strategy/strats/hyperoptable_strategy.py b/tests/strategy/strats/hyperoptable_strategy.py index dc6b03a3e..88bdd078e 100644 --- a/tests/strategy/strats/hyperoptable_strategy.py +++ b/tests/strategy/strats/hyperoptable_strategy.py @@ -1,13 +1,14 @@ # pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement +import talib.abstract as ta from pandas import DataFrame -from strategy_test_v2 import StrategyTestV2 import freqtrade.vendor.qtpylib.indicators as qtpylib -from freqtrade.strategy import BooleanParameter, DecimalParameter, IntParameter, RealParameter +from freqtrade.strategy import (BooleanParameter, DecimalParameter, IntParameter, IStrategy, + RealParameter) -class HyperoptableStrategy(StrategyTestV2): +class HyperoptableStrategy(IStrategy): """ Default Strategy provided by freqtrade bot. Please do not modify this strategy, it's intended for internal use only. @@ -15,6 +16,38 @@ class HyperoptableStrategy(StrategyTestV2): or strategy repository https://github.com/freqtrade/freqtrade-strategies for samples and inspiration. """ + INTERFACE_VERSION = 2 + + # Minimal ROI designed for the strategy + minimal_roi = { + "40": 0.0, + "30": 0.01, + "20": 0.02, + "0": 0.04 + } + + # Optimal stoploss designed for the strategy + stoploss = -0.10 + + # Optimal ticker interval for the strategy + timeframe = '5m' + + # Optional order type mapping + order_types = { + 'buy': 'limit', + 'sell': 'limit', + 'stoploss': 'limit', + 'stoploss_on_exchange': False + } + + # Number of candles the strategy requires before producing valid signals + startup_candle_count: int = 20 + + # Optional time in force for orders + order_time_in_force = { + 'buy': 'gtc', + 'sell': 'gtc', + } buy_params = { 'buy_rsi': 35, @@ -58,6 +91,55 @@ class HyperoptableStrategy(StrategyTestV2): """ return [] + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Adds several different TA indicators to the given DataFrame + + Performance Note: For the best performance be frugal on the number of indicators + you are using. Let uncomment only the indicator you are using in your strategies + or your hyperopt configuration, otherwise you will waste your memory and CPU usage. + :param dataframe: Dataframe with data from the exchange + :param metadata: Additional information, like the currently traded pair + :return: a Dataframe with all mandatory indicators for the strategies + """ + + # Momentum Indicator + # ------------------------------------ + + # ADX + dataframe['adx'] = ta.ADX(dataframe) + + # MACD + macd = ta.MACD(dataframe) + dataframe['macd'] = macd['macd'] + dataframe['macdsignal'] = macd['macdsignal'] + dataframe['macdhist'] = macd['macdhist'] + + # Minus Directional Indicator / Movement + dataframe['minus_di'] = ta.MINUS_DI(dataframe) + + # Plus Directional Indicator / Movement + dataframe['plus_di'] = ta.PLUS_DI(dataframe) + + # RSI + dataframe['rsi'] = ta.RSI(dataframe) + + # Stoch fast + stoch_fast = ta.STOCHF(dataframe) + dataframe['fastd'] = stoch_fast['fastd'] + dataframe['fastk'] = stoch_fast['fastk'] + + # Bollinger bands + bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2) + dataframe['bb_lowerband'] = bollinger['lower'] + dataframe['bb_middleband'] = bollinger['mid'] + dataframe['bb_upperband'] = bollinger['upper'] + + # EMA - Exponential Moving Average + dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10) + + return dataframe + def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Based on TA indicators, populates the buy signal for the given dataframe diff --git a/tests/strategy/strats/strategy_test_v2.py b/tests/strategy/strats/strategy_test_v2.py index 59f1f569e..c57becdad 100644 --- a/tests/strategy/strats/strategy_test_v2.py +++ b/tests/strategy/strats/strategy_test_v2.py @@ -7,7 +7,7 @@ from pandas import DataFrame import freqtrade.vendor.qtpylib.indicators as qtpylib from freqtrade.persistence import Trade -from freqtrade.strategy import IStrategy +from freqtrade.strategy.interface import IStrategy class StrategyTestV2(IStrategy): From 3e24d01af401dd1fbe11b9c3951a6e848f8af716 Mon Sep 17 00:00:00 2001 From: Italo <45588475+italodamato@users.noreply.github.com> Date: Wed, 30 Mar 2022 09:41:40 +0100 Subject: [PATCH 025/118] fix flake8 --- tests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index 1dd6e8869..0387f0a22 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2089,7 +2089,7 @@ def saved_hyperopt_results(): 'params_details': {'buy': {'mfi-value': 13, 'fastd-value': 35, 'adx-value': 39, 'rsi-value': 29, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 87, 'sell-fastd-value': 54, 'sell-adx-value': 63, 'sell-rsi-value': 93, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.411946348378729, 215: 0.2052334363683207, 891: 0.06264755784937427, 2293: 0}, 'stoploss': {'stoploss': -0.11818343570194478}}, # noqa: E501 'results_metrics': {'total_trades': 0, 'wins': 0, 'draws': 0, 'losses': 0, 'profit_mean': None, 'profit_median': None, 'profit_total': 0, 'profit': 0.0, 'holding_avg': timedelta()}, # noqa: E501 'results_explanation': ' 0 trades. Avg profit nan%. Total profit 0.00000000 BTC ( 0.00Σ%). Avg duration nan min.', # noqa: E501 - 'total_profit': 0, 'current_epoch': 4, 'is_initial_point': True, 'is_random': False, 'is_best': False + 'total_profit': 0, 'current_epoch': 4, 'is_initial_point': True, 'is_random': False, 'is_best': False # noqa: E501 }, { 'loss': 0.22195522184191518, 'params_dict': {'mfi-value': 17, 'fastd-value': 21, 'adx-value': 38, 'rsi-value': 33, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': False, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 87, 'sell-fastd-value': 82, 'sell-adx-value': 78, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 1269, 'roi_t2': 601, 'roi_t3': 444, 'roi_p1': 0.07280999507931168, 'roi_p2': 0.08946698095898986, 'roi_p3': 0.1454876733325284, 'stoploss': -0.18181041180901014}, # noqa: E501 From bad179ebaa605927e956e2bf6e7d8391538b9f31 Mon Sep 17 00:00:00 2001 From: Italo <45588475+italodamato@users.noreply.github.com> Date: Wed, 30 Mar 2022 09:48:10 +0100 Subject: [PATCH 026/118] fix merge mess This reverts commit 9f171193ef2b893b7f7c9269b9a2c6b796f0d71c. --- .github/workflows/ci.yml | 10 +-- .github/workflows/docker_update_readme.yml | 2 +- docs/includes/pricing.md | 8 +- docs/requirements-docs.txt | 7 +- docs/strategy-advanced.md | 17 ++-- freqtrade/__init__.py | 17 +--- freqtrade/resolvers/iresolver.py | 60 +++++++++---- requirements-dev.txt | 12 +-- requirements.txt | 8 +- tests/conftest.py | 30 ++++--- tests/exchange/test_exchange.py | 2 +- .../strategy/strats/hyperoptable_strategy.py | 88 +------------------ tests/strategy/strats/strategy_test_v2.py | 2 +- 13 files changed, 97 insertions(+), 166 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 216a53bc1..b8df7ab10 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,14 +31,14 @@ jobs: python-version: ${{ matrix.python-version }} - name: Cache_dependencies - uses: actions/cache@v2 + uses: actions/cache@v3 id: cache with: path: ~/dependencies/ key: ${{ runner.os }}-dependencies - name: pip cache (linux) - uses: actions/cache@v2 + uses: actions/cache@v3 if: runner.os == 'Linux' with: path: ~/.cache/pip @@ -126,14 +126,14 @@ jobs: python-version: ${{ matrix.python-version }} - name: Cache_dependencies - uses: actions/cache@v2 + uses: actions/cache@v3 id: cache with: path: ~/dependencies/ key: ${{ runner.os }}-dependencies - name: pip cache (macOS) - uses: actions/cache@v2 + uses: actions/cache@v3 if: runner.os == 'macOS' with: path: ~/Library/Caches/pip @@ -218,7 +218,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Pip cache (Windows) - uses: actions/cache@preview + uses: actions/cache@v3 with: path: ~\AppData\Local\pip\Cache key: ${{ matrix.os }}-${{ matrix.python-version }}-pip diff --git a/.github/workflows/docker_update_readme.yml b/.github/workflows/docker_update_readme.yml index ebb773ad7..822533ee2 100644 --- a/.github/workflows/docker_update_readme.yml +++ b/.github/workflows/docker_update_readme.yml @@ -10,7 +10,7 @@ jobs: steps: - uses: actions/checkout@v3 - name: Docker Hub Description - uses: peter-evans/dockerhub-description@v2.4.3 + uses: peter-evans/dockerhub-description@v3 env: DOCKERHUB_USERNAME: ${{ secrets.DOCKER_USERNAME }} DOCKERHUB_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} diff --git a/docs/includes/pricing.md b/docs/includes/pricing.md index ed8a45e68..103df6cd3 100644 --- a/docs/includes/pricing.md +++ b/docs/includes/pricing.md @@ -51,9 +51,9 @@ When buying with the orderbook enabled (`bid_strategy.use_order_book=True`), Fre #### Buy price without Orderbook enabled -The following section uses `side` as the configured `bid_strategy.price_side`. +The following section uses `side` as the configured `bid_strategy.price_side` (defaults to `"bid"`). -When not using orderbook (`bid_strategy.use_order_book=False`), Freqtrade uses the best `side` price from the ticker if it's below the `last` traded price from the ticker. Otherwise (when the `side` price is above the `last` price), it calculates a rate between `side` and `last` price. +When not using orderbook (`bid_strategy.use_order_book=False`), Freqtrade uses the best `side` price from the ticker if it's below the `last` traded price from the ticker. Otherwise (when the `side` price is above the `last` price), it calculates a rate between `side` and `last` price based on `bid_strategy.ask_last_balance`.. The `bid_strategy.ask_last_balance` configuration parameter controls this. A value of `0.0` will use `side` price, while `1.0` will use the `last` price and values between those interpolate between ask and last price. @@ -88,9 +88,9 @@ When selling with the orderbook enabled (`ask_strategy.use_order_book=True`), Fr #### Sell price without Orderbook enabled -When not using orderbook (`ask_strategy.use_order_book=False`), the price at the `ask_strategy.price_side` side (defaults to `"ask"`) from the ticker will be used as the sell price. +The following section uses `side` as the configured `ask_strategy.price_side` (defaults to `"ask"`). -When not using orderbook (`ask_strategy.use_order_book=False`), Freqtrade uses the best `side` price from the ticker if it's below the `last` traded price from the ticker. Otherwise (when the `side` price is above the `last` price), it calculates a rate between `side` and `last` price. +When not using orderbook (`ask_strategy.use_order_book=False`), Freqtrade uses the best `side` price from the ticker if it's above the `last` traded price from the ticker. Otherwise (when the `side` price is below the `last` price), it calculates a rate between `side` and `last` price based on `ask_strategy.bid_last_balance`. The `ask_strategy.bid_last_balance` configuration parameter controls this. A value of `0.0` will use `side` price, while `1.0` will use the last price and values between those interpolate between `side` and last price. diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index 0ca0e4b63..1f7db75c5 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,4 +1,5 @@ -mkdocs==1.2.3 -mkdocs-material==8.2.5 +mkdocs==1.3.0 +mkdocs-material==8.2.8 mdx_truly_sane_lists==1.2 -pymdown-extensions==9.2 +pymdown-extensions==9.3 +jinja2==3.1.1 diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 3793abacf..b1f154355 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -146,7 +146,7 @@ def version(self) -> str: The strategies can be derived from other strategies. This avoids duplication of your custom strategy code. You can use this technique to override small parts of your main strategy, leaving the rest untouched: -``` python +``` python title="user_data/strategies/myawesomestrategy.py" class MyAwesomeStrategy(IStrategy): ... stoploss = 0.13 @@ -155,6 +155,10 @@ class MyAwesomeStrategy(IStrategy): # should be in any custom strategy... ... +``` + +``` python title="user_data/strategies/MyAwesomeStrategy2.py" +from myawesomestrategy import MyAwesomeStrategy class MyAwesomeStrategy2(MyAwesomeStrategy): # Override something stoploss = 0.08 @@ -163,16 +167,7 @@ class MyAwesomeStrategy2(MyAwesomeStrategy): Both attributes and methods may be overridden, altering behavior of the original strategy in a way you need. -!!! Note "Parent-strategy in different files" - If you have the parent-strategy in a different file, you'll need to add the following to the top of your "child"-file to ensure proper loading, otherwise freqtrade may not be able to load the parent strategy correctly. - - ``` python - import sys - from pathlib import Path - sys.path.append(str(Path(__file__).parent)) - - from myawesomestrategy import MyAwesomeStrategy - ``` +While keeping the subclass in the same file is technically possible, it can lead to some problems with hyperopt parameter files, we therefore recommend to use separate strategy files, and import the parent strategy as shown above. ## Embedding Strategies diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index 2747efc96..f8be8f66f 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,27 +1,14 @@ """ Freqtrade bot """ __version__ = 'develop' -if __version__ == 'develop': - +if 'dev' in __version__: try: import subprocess - __version__ = 'develop-' + subprocess.check_output( + __version__ = __version__ + '-' + subprocess.check_output( ['git', 'log', '--format="%h"', '-n 1'], stderr=subprocess.DEVNULL).decode("utf-8").rstrip().strip('"') - # from datetime import datetime - # last_release = subprocess.check_output( - # ['git', 'tag'] - # ).decode('utf-8').split()[-1].split(".") - # # Releases are in the format "2020.1" - we increment the latest version for dev. - # prefix = f"{last_release[0]}.{int(last_release[1]) + 1}" - # dev_version = int(datetime.now().timestamp() // 1000) - # __version__ = f"{prefix}.dev{dev_version}" - - # subprocess.check_output( - # ['git', 'log', '--format="%h"', '-n 1'], - # stderr=subprocess.DEVNULL).decode("utf-8").rstrip().strip('"') except Exception: # pragma: no cover # git not available, ignore try: diff --git a/freqtrade/resolvers/iresolver.py b/freqtrade/resolvers/iresolver.py index c6f97c976..3ab461041 100644 --- a/freqtrade/resolvers/iresolver.py +++ b/freqtrade/resolvers/iresolver.py @@ -6,6 +6,7 @@ This module load custom objects import importlib.util import inspect import logging +import sys from pathlib import Path from typing import Any, Dict, Iterator, List, Optional, Tuple, Type, Union @@ -15,6 +16,22 @@ from freqtrade.exceptions import OperationalException logger = logging.getLogger(__name__) +class PathModifier: + def __init__(self, path: Path): + self.path = path + + def __enter__(self): + """Inject path to allow importing with relative imports.""" + sys.path.insert(0, str(self.path)) + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + """Undo insertion of local path.""" + str_path = str(self.path) + if str_path in sys.path: + sys.path.remove(str_path) + + class IResolver: """ This class contains all the logic to load custom classes @@ -57,27 +74,32 @@ class IResolver: # Generate spec based on absolute path # Pass object_name as first argument to have logging print a reasonable name. - spec = importlib.util.spec_from_file_location(object_name or "", str(module_path)) - if not spec: - return iter([None]) - - module = importlib.util.module_from_spec(spec) - try: - spec.loader.exec_module(module) # type: ignore # importlib does not use typehints - except (ModuleNotFoundError, SyntaxError, ImportError, NameError) as err: - # Catch errors in case a specific module is not installed - logger.warning(f"Could not import {module_path} due to '{err}'") - if enum_failed: + with PathModifier(module_path.parent): + module_name = module_path.stem or "" + spec = importlib.util.spec_from_file_location(module_name, str(module_path)) + if not spec: return iter([None]) - valid_objects_gen = ( - (obj, inspect.getsource(module)) for - name, obj in inspect.getmembers( - module, inspect.isclass) if ((object_name is None or object_name == name) - and issubclass(obj, cls.object_type) - and obj is not cls.object_type) - ) - return valid_objects_gen + module = importlib.util.module_from_spec(spec) + try: + spec.loader.exec_module(module) # type: ignore # importlib does not use typehints + except (ModuleNotFoundError, SyntaxError, ImportError, NameError) as err: + # Catch errors in case a specific module is not installed + logger.warning(f"Could not import {module_path} due to '{err}'") + if enum_failed: + return iter([None]) + + valid_objects_gen = ( + (obj, inspect.getsource(module)) for + name, obj in inspect.getmembers( + module, inspect.isclass) if ((object_name is None or object_name == name) + and issubclass(obj, cls.object_type) + and obj is not cls.object_type + and obj.__module__ == module_name + ) + ) + # The __module__ check ensures we only use strategies that are defined in this folder. + return valid_objects_gen @classmethod def _search_object(cls, directory: Path, *, object_name: str, add_source: bool = False diff --git a/requirements-dev.txt b/requirements-dev.txt index c2f3eae8a..063cfaa45 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -6,9 +6,9 @@ coveralls==3.3.1 flake8==4.0.1 flake8-tidy-imports==4.6.0 -mypy==0.940 -pytest==7.1.0 -pytest-asyncio==0.18.2 +mypy==0.942 +pytest==7.1.1 +pytest-asyncio==0.18.3 pytest-cov==3.0.0 pytest-mock==3.7.0 pytest-random-order==1.0.4 @@ -22,8 +22,8 @@ nbconvert==6.4.4 # mypy types types-cachetools==5.0.0 types-filelock==3.2.5 -types-requests==2.27.12 -types-tabulate==0.8.5 +types-requests==2.27.15 +types-tabulate==0.8.6 # Extensions to datetime library -types-python-dateutil==2.8.9 \ No newline at end of file +types-python-dateutil==2.8.10 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index f0f030e78..de05b3f7c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,22 +2,22 @@ numpy==1.22.3 pandas==1.4.1 pandas-ta==0.3.14b -ccxt==1.76.5 +ccxt==1.77.36 # Pin cryptography for now due to rust build errors with piwheels -cryptography==36.0.1 +cryptography==36.0.2 aiohttp==3.8.1 SQLAlchemy==1.4.32 python-telegram-bot==13.11 arrow==1.2.2 cachetools==4.2.2 requests==2.27.1 -urllib3==1.26.8 +urllib3==1.26.9 jsonschema==4.4.0 TA-Lib==0.4.24 technical==1.3.0 tabulate==0.8.9 pycoingecko==2.2.0 -jinja2==3.0.3 +jinja2==3.1.1 tables==3.7.0 blosc==1.10.6 diff --git a/tests/conftest.py b/tests/conftest.py index 0387f0a22..b15716fcc 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1019,8 +1019,8 @@ def limit_buy_order_open(): 'type': 'limit', 'side': 'buy', 'symbol': 'mocked', + 'timestamp': arrow.utcnow().int_timestamp * 1000, 'datetime': arrow.utcnow().isoformat(), - 'timestamp': arrow.utcnow().int_timestamp, 'price': 0.00001099, 'amount': 90.99181073, 'filled': 0.0, @@ -1046,6 +1046,7 @@ def market_buy_order(): 'type': 'market', 'side': 'buy', 'symbol': 'mocked', + 'timestamp': arrow.utcnow().int_timestamp * 1000, 'datetime': arrow.utcnow().isoformat(), 'price': 0.00004099, 'amount': 91.99181073, @@ -1062,6 +1063,7 @@ def market_sell_order(): 'type': 'market', 'side': 'sell', 'symbol': 'mocked', + 'timestamp': arrow.utcnow().int_timestamp * 1000, 'datetime': arrow.utcnow().isoformat(), 'price': 0.00004173, 'amount': 91.99181073, @@ -1078,7 +1080,8 @@ def limit_buy_order_old(): 'type': 'limit', 'side': 'buy', 'symbol': 'mocked', - 'datetime': str(arrow.utcnow().shift(minutes=-601).datetime), + 'datetime': arrow.utcnow().shift(minutes=-601).isoformat(), + 'timestamp': arrow.utcnow().shift(minutes=-601).int_timestamp * 1000, 'price': 0.00001099, 'amount': 90.99181073, 'filled': 0.0, @@ -1094,6 +1097,7 @@ def limit_sell_order_old(): 'type': 'limit', 'side': 'sell', 'symbol': 'ETH/BTC', + 'timestamp': arrow.utcnow().shift(minutes=-601).int_timestamp * 1000, 'datetime': arrow.utcnow().shift(minutes=-601).isoformat(), 'price': 0.00001099, 'amount': 90.99181073, @@ -1110,6 +1114,7 @@ def limit_buy_order_old_partial(): 'type': 'limit', 'side': 'buy', 'symbol': 'ETH/BTC', + 'timestamp': arrow.utcnow().shift(minutes=-601).int_timestamp * 1000, 'datetime': arrow.utcnow().shift(minutes=-601).isoformat(), 'price': 0.00001099, 'amount': 90.99181073, @@ -1139,7 +1144,7 @@ def limit_buy_order_canceled_empty(request): 'info': {}, 'id': '1234512345', 'clientOrderId': None, - 'timestamp': arrow.utcnow().shift(minutes=-601).int_timestamp, + 'timestamp': arrow.utcnow().shift(minutes=-601).int_timestamp * 1000, 'datetime': arrow.utcnow().shift(minutes=-601).isoformat(), 'lastTradeTimestamp': None, 'symbol': 'LTC/USDT', @@ -1160,7 +1165,7 @@ def limit_buy_order_canceled_empty(request): 'info': {}, 'id': 'AZNPFF-4AC4N-7MKTAT', 'clientOrderId': None, - 'timestamp': arrow.utcnow().shift(minutes=-601).int_timestamp, + 'timestamp': arrow.utcnow().shift(minutes=-601).int_timestamp * 1000, 'datetime': arrow.utcnow().shift(minutes=-601).isoformat(), 'lastTradeTimestamp': None, 'status': 'canceled', @@ -1181,7 +1186,7 @@ def limit_buy_order_canceled_empty(request): 'info': {}, 'id': '1234512345', 'clientOrderId': 'alb1234123', - 'timestamp': arrow.utcnow().shift(minutes=-601).int_timestamp, + 'timestamp': arrow.utcnow().shift(minutes=-601).int_timestamp * 1000, 'datetime': arrow.utcnow().shift(minutes=-601).isoformat(), 'lastTradeTimestamp': None, 'symbol': 'LTC/USDT', @@ -1202,7 +1207,7 @@ def limit_buy_order_canceled_empty(request): 'info': {}, 'id': '1234512345', 'clientOrderId': 'alb1234123', - 'timestamp': arrow.utcnow().shift(minutes=-601).int_timestamp, + 'timestamp': arrow.utcnow().shift(minutes=-601).int_timestamp * 1000, 'datetime': arrow.utcnow().shift(minutes=-601).isoformat(), 'lastTradeTimestamp': None, 'symbol': 'LTC/USDT', @@ -1228,7 +1233,7 @@ def limit_sell_order_open(): 'side': 'sell', 'symbol': 'mocked', 'datetime': arrow.utcnow().isoformat(), - 'timestamp': arrow.utcnow().int_timestamp, + 'timestamp': arrow.utcnow().int_timestamp * 1000, 'price': 0.00001173, 'amount': 90.99181073, 'filled': 0.0, @@ -1394,7 +1399,7 @@ def tickers(): 'BLK/BTC': { 'symbol': 'BLK/BTC', 'timestamp': 1522014806072, - 'datetime': '2018-03-25T21:53:26.720Z', + 'datetime': '2018-03-25T21:53:26.072Z', 'high': 0.007745, 'low': 0.007512, 'bid': 0.007729, @@ -1890,7 +1895,8 @@ def buy_order_fee(): 'type': 'limit', 'side': 'buy', 'symbol': 'mocked', - 'datetime': str(arrow.utcnow().shift(minutes=-601).datetime), + 'timestamp': arrow.utcnow().shift(minutes=-601).int_timestamp * 1000, + 'datetime': arrow.utcnow().shift(minutes=-601).isoformat(), 'price': 0.245441, 'amount': 8.0, 'cost': 1.963528, @@ -2199,7 +2205,7 @@ def limit_buy_order_usdt_open(): 'side': 'buy', 'symbol': 'mocked', 'datetime': arrow.utcnow().isoformat(), - 'timestamp': arrow.utcnow().int_timestamp, + 'timestamp': arrow.utcnow().int_timestamp * 1000, 'price': 2.00, 'amount': 30.0, 'filled': 0.0, @@ -2226,7 +2232,7 @@ def limit_sell_order_usdt_open(): 'side': 'sell', 'symbol': 'mocked', 'datetime': arrow.utcnow().isoformat(), - 'timestamp': arrow.utcnow().int_timestamp, + 'timestamp': arrow.utcnow().int_timestamp * 1000, 'price': 2.20, 'amount': 30.0, 'filled': 0.0, @@ -2251,6 +2257,7 @@ def market_buy_order_usdt(): 'type': 'market', 'side': 'buy', 'symbol': 'mocked', + 'timestamp': arrow.utcnow().int_timestamp * 1000, 'datetime': arrow.utcnow().isoformat(), 'price': 2.00, 'amount': 30.0, @@ -2307,6 +2314,7 @@ def market_sell_order_usdt(): 'type': 'market', 'side': 'sell', 'symbol': 'mocked', + 'timestamp': arrow.utcnow().int_timestamp * 1000, 'datetime': arrow.utcnow().isoformat(), 'price': 2.20, 'amount': 30.0, diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index ff8383997..b76cb23e6 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1098,7 +1098,7 @@ def test_create_order(default_conf, mocker, side, ordertype, rate, marketprice, exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) order = exchange.create_order( - pair='ETH/BTC', ordertype=ordertype, side=side, amount=1, rate=200) + pair='ETH/BTC', ordertype=ordertype, side=side, amount=1, rate=rate) assert 'id' in order assert 'info' in order diff --git a/tests/strategy/strats/hyperoptable_strategy.py b/tests/strategy/strats/hyperoptable_strategy.py index 88bdd078e..dc6b03a3e 100644 --- a/tests/strategy/strats/hyperoptable_strategy.py +++ b/tests/strategy/strats/hyperoptable_strategy.py @@ -1,14 +1,13 @@ # pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement -import talib.abstract as ta from pandas import DataFrame +from strategy_test_v2 import StrategyTestV2 import freqtrade.vendor.qtpylib.indicators as qtpylib -from freqtrade.strategy import (BooleanParameter, DecimalParameter, IntParameter, IStrategy, - RealParameter) +from freqtrade.strategy import BooleanParameter, DecimalParameter, IntParameter, RealParameter -class HyperoptableStrategy(IStrategy): +class HyperoptableStrategy(StrategyTestV2): """ Default Strategy provided by freqtrade bot. Please do not modify this strategy, it's intended for internal use only. @@ -16,38 +15,6 @@ class HyperoptableStrategy(IStrategy): or strategy repository https://github.com/freqtrade/freqtrade-strategies for samples and inspiration. """ - INTERFACE_VERSION = 2 - - # Minimal ROI designed for the strategy - minimal_roi = { - "40": 0.0, - "30": 0.01, - "20": 0.02, - "0": 0.04 - } - - # Optimal stoploss designed for the strategy - stoploss = -0.10 - - # Optimal ticker interval for the strategy - timeframe = '5m' - - # Optional order type mapping - order_types = { - 'buy': 'limit', - 'sell': 'limit', - 'stoploss': 'limit', - 'stoploss_on_exchange': False - } - - # Number of candles the strategy requires before producing valid signals - startup_candle_count: int = 20 - - # Optional time in force for orders - order_time_in_force = { - 'buy': 'gtc', - 'sell': 'gtc', - } buy_params = { 'buy_rsi': 35, @@ -91,55 +58,6 @@ class HyperoptableStrategy(IStrategy): """ return [] - def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: - """ - Adds several different TA indicators to the given DataFrame - - Performance Note: For the best performance be frugal on the number of indicators - you are using. Let uncomment only the indicator you are using in your strategies - or your hyperopt configuration, otherwise you will waste your memory and CPU usage. - :param dataframe: Dataframe with data from the exchange - :param metadata: Additional information, like the currently traded pair - :return: a Dataframe with all mandatory indicators for the strategies - """ - - # Momentum Indicator - # ------------------------------------ - - # ADX - dataframe['adx'] = ta.ADX(dataframe) - - # MACD - macd = ta.MACD(dataframe) - dataframe['macd'] = macd['macd'] - dataframe['macdsignal'] = macd['macdsignal'] - dataframe['macdhist'] = macd['macdhist'] - - # Minus Directional Indicator / Movement - dataframe['minus_di'] = ta.MINUS_DI(dataframe) - - # Plus Directional Indicator / Movement - dataframe['plus_di'] = ta.PLUS_DI(dataframe) - - # RSI - dataframe['rsi'] = ta.RSI(dataframe) - - # Stoch fast - stoch_fast = ta.STOCHF(dataframe) - dataframe['fastd'] = stoch_fast['fastd'] - dataframe['fastk'] = stoch_fast['fastk'] - - # Bollinger bands - bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2) - dataframe['bb_lowerband'] = bollinger['lower'] - dataframe['bb_middleband'] = bollinger['mid'] - dataframe['bb_upperband'] = bollinger['upper'] - - # EMA - Exponential Moving Average - dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10) - - return dataframe - def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Based on TA indicators, populates the buy signal for the given dataframe diff --git a/tests/strategy/strats/strategy_test_v2.py b/tests/strategy/strats/strategy_test_v2.py index c57becdad..59f1f569e 100644 --- a/tests/strategy/strats/strategy_test_v2.py +++ b/tests/strategy/strats/strategy_test_v2.py @@ -7,7 +7,7 @@ from pandas import DataFrame import freqtrade.vendor.qtpylib.indicators as qtpylib from freqtrade.persistence import Trade -from freqtrade.strategy.interface import IStrategy +from freqtrade.strategy import IStrategy class StrategyTestV2(IStrategy): From e85c7ca8ff18f7f139ddef49a0d21a5a71ecd058 Mon Sep 17 00:00:00 2001 From: Italo <45588475+italodamato@users.noreply.github.com> Date: Wed, 30 Mar 2022 09:50:37 +0100 Subject: [PATCH 027/118] remove blank line --- requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 03a9da87b..b116b261f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,6 @@ numpy==1.22.3 pandas==1.4.1 pandas-ta==0.3.14b - ccxt==1.77.45 # Pin cryptography for now due to rust build errors with piwheels cryptography==36.0.2 From 6df15a7af9aa59740af1802fd4e2b7da02132279 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sat, 19 Mar 2022 18:54:11 -0600 Subject: [PATCH 028/118] Recursively search subdirectories in user_data/strategies for a strategy --- freqtrade/resolvers/iresolver.py | 13 +++++++++---- freqtrade/resolvers/strategy_resolver.py | 10 ++++++++-- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/freqtrade/resolvers/iresolver.py b/freqtrade/resolvers/iresolver.py index 3ab461041..cddc8b84d 100644 --- a/freqtrade/resolvers/iresolver.py +++ b/freqtrade/resolvers/iresolver.py @@ -44,7 +44,7 @@ class IResolver: @classmethod def build_search_paths(cls, config: Dict[str, Any], user_subdir: Optional[str] = None, - extra_dir: Optional[str] = None) -> List[Path]: + extra_dirs: Optional[List[str]] = None) -> List[Path]: abs_paths: List[Path] = [] if cls.initial_search_path: @@ -53,9 +53,10 @@ class IResolver: if user_subdir: abs_paths.insert(0, config['user_data_dir'].joinpath(user_subdir)) - if extra_dir: + if extra_dirs: # Add extra directory to the top of the search paths - abs_paths.insert(0, Path(extra_dir).resolve()) + for dir in extra_dirs: + abs_paths.insert(0, Path(dir).resolve()) return abs_paths @@ -164,9 +165,13 @@ class IResolver: :return: Object instance or None """ + extra_dirs: List[str] = [] + if extra_dir: + extra_dirs.append(extra_dir) + abs_paths = cls.build_search_paths(config, user_subdir=cls.user_subdir, - extra_dir=extra_dir) + extra_dirs=extra_dirs) found_object = cls._load_object(paths=abs_paths, object_name=object_name, kwargs=kwargs) diff --git a/freqtrade/resolvers/strategy_resolver.py b/freqtrade/resolvers/strategy_resolver.py index 8ad7cdb59..4f5d22e45 100644 --- a/freqtrade/resolvers/strategy_resolver.py +++ b/freqtrade/resolvers/strategy_resolver.py @@ -7,8 +7,9 @@ import logging import tempfile from base64 import urlsafe_b64decode from inspect import getfullargspec +from os import walk from pathlib import Path -from typing import Any, Dict, Optional +from typing import Any, Dict, List, Optional from freqtrade.constants import REQUIRED_ORDERTIF, REQUIRED_ORDERTYPES, USERPATH_STRATEGIES from freqtrade.exceptions import OperationalException @@ -166,10 +167,15 @@ class StrategyResolver(IResolver): :param extra_dir: additional directory to search for the given strategy :return: Strategy instance or None """ + extra_dirs: List[str] = [ + path[0] for path in walk(f"{config['user_data_dir']}/{USERPATH_STRATEGIES}") + ] # sub-directories + if extra_dir: + extra_dirs.append(extra_dir) abs_paths = StrategyResolver.build_search_paths(config, user_subdir=USERPATH_STRATEGIES, - extra_dir=extra_dir) + extra_dirs=extra_dirs) if ":" in strategy_name: logger.info("loading base64 encoded strategy") From 185daf5772fc1e42d014898eedc8bbbebd4bcfb8 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sun, 20 Mar 2022 03:02:14 -0600 Subject: [PATCH 029/118] add recursive command line option --- freqtrade/commands/cli_options.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index f30c25ba1..955c1ae53 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -82,6 +82,11 @@ AVAILABLE_CLI_OPTIONS = { help='Reset sample files to their original state.', action='store_true', ), + "recursive": Arg( + '-r', '--recursive', + help='Recursively search for a strategy in the strategies folder.', + metavar='store_true', + ), # Main options "strategy": Arg( '-s', '--strategy', From f44ae494fb0c51510d27176d21cc17fd201b7234 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Thu, 31 Mar 2022 08:11:05 -0600 Subject: [PATCH 030/118] Added recursive to configuration --- freqtrade/configuration/configuration.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 1ba17a04d..916c2b675 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -267,6 +267,12 @@ class Configuration: self._args_to_config(config, argname='strategy_list', logstring='Using strategy list of {} strategies', logfun=len) + self._args_to_config( + config, + argname='recursive', + logstring='Recursively searching for a strategy in the strategies folder.', + ) + self._args_to_config(config, argname='timeframe', logstring='Overriding timeframe with Command line argument') From b4b809ff8e19e3a0918cc8ff9e6ad1dbbb538077 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Thu, 31 Mar 2022 08:16:21 -0600 Subject: [PATCH 031/118] changed recursive to recursive_strategy_search --- freqtrade/commands/cli_options.py | 4 ++-- freqtrade/configuration/configuration.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index 955c1ae53..2ed42b299 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -82,8 +82,8 @@ AVAILABLE_CLI_OPTIONS = { help='Reset sample files to their original state.', action='store_true', ), - "recursive": Arg( - '-r', '--recursive', + "recursive_strategy_search": Arg( + '-r', '--recursive_strategy_search', help='Recursively search for a strategy in the strategies folder.', metavar='store_true', ), diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 916c2b675..ae3ed45be 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -269,7 +269,7 @@ class Configuration: self._args_to_config( config, - argname='recursive', + argname='recursive_strategy_search', logstring='Recursively searching for a strategy in the strategies folder.', ) From 2fe5a1594f153b68bc576ebaaa0ceeb8397d5279 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Thu, 31 Mar 2022 08:16:41 -0600 Subject: [PATCH 032/118] Add conditional to recursive strategy searching if in config --- freqtrade/resolvers/strategy_resolver.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/freqtrade/resolvers/strategy_resolver.py b/freqtrade/resolvers/strategy_resolver.py index 4f5d22e45..44f02e232 100644 --- a/freqtrade/resolvers/strategy_resolver.py +++ b/freqtrade/resolvers/strategy_resolver.py @@ -167,9 +167,13 @@ class StrategyResolver(IResolver): :param extra_dir: additional directory to search for the given strategy :return: Strategy instance or None """ - extra_dirs: List[str] = [ - path[0] for path in walk(f"{config['user_data_dir']}/{USERPATH_STRATEGIES}") - ] # sub-directories + if config['recursive_strategy_search']: + extra_dirs: List[str] = [ + path[0] for path in walk(f"{config['user_data_dir']}/{USERPATH_STRATEGIES}") + ] # sub-directories + else: + extra_dirs = [] + if extra_dir: extra_dirs.append(extra_dir) From 82e9f62381d4747dc87994349646b261e9a7a88d Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 5 Apr 2022 20:27:32 +0200 Subject: [PATCH 033/118] Add missing setting in arguments.py --- freqtrade/commands/arguments.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index 28f7d7148..2fb8d3258 100644 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -12,7 +12,7 @@ from freqtrade.constants import DEFAULT_CONFIG ARGS_COMMON = ["verbosity", "logfile", "version", "config", "datadir", "user_data_dir"] -ARGS_STRATEGY = ["strategy", "strategy_path"] +ARGS_STRATEGY = ["strategy", "strategy_path", "recursive_strategy_search"] ARGS_TRADE = ["db_url", "sd_notify", "dry_run", "dry_run_wallet", "fee"] From 1559692e4729115af3d63225a98e090e013d74c7 Mon Sep 17 00:00:00 2001 From: Italo <45588475+italodamato@users.noreply.github.com> Date: Fri, 8 Apr 2022 11:44:42 +0100 Subject: [PATCH 034/118] Update hyperopt.py remove duplicates from list of asked points --- freqtrade/optimize/hyperopt.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 35f382469..2883199a9 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -422,16 +422,23 @@ class Hyperopt: 5. Repeat until at least `n_points` points in the `asked_non_tried` list 6. Return a list with length truncated at `n_points` ''' + def unique_list(a_list): + seen = [] + for x in a_list: + key = repr(x) + if key not in seen: + seen.append(eval(key)) + return seen i = 0 asked_non_tried: List[List[Any]] = [] is_random: List[bool] = [] while i < 5 and len(asked_non_tried) < n_points: if i < 3: self.opt.cache_ = {} - asked = self.opt.ask(n_points=n_points * 5) + asked = unique_list(self.opt.ask(n_points=n_points * 5)) is_random = [False for _ in range(len(asked))] else: - asked = self.opt.space.rvs(n_samples=n_points * 5) + asked = unique_list(self.opt.space.rvs(n_samples=n_points * 5)) is_random = [True for _ in range(len(asked))] asked_non_tried += [x for x in asked if x not in self.opt.Xi From d5ce868f1aa1d99c0f7d936cc88161611dc8aa4c Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sun, 10 Apr 2022 18:44:33 -0600 Subject: [PATCH 035/118] removed 1 letter alias for recursive-strategy-folder --- freqtrade/commands/cli_options.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index 2ed42b299..095aad6c3 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -83,7 +83,7 @@ AVAILABLE_CLI_OPTIONS = { action='store_true', ), "recursive_strategy_search": Arg( - '-r', '--recursive_strategy_search', + '--recursive_strategy_search', help='Recursively search for a strategy in the strategies folder.', metavar='store_true', ), From c876d42e369baae4d86d7abd2db035454a6904b9 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sun, 10 Apr 2022 18:50:51 -0600 Subject: [PATCH 036/118] safe check for recursive_strategy_search in strategy_resolver --- freqtrade/resolvers/strategy_resolver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/resolvers/strategy_resolver.py b/freqtrade/resolvers/strategy_resolver.py index 44f02e232..8a22dbd65 100644 --- a/freqtrade/resolvers/strategy_resolver.py +++ b/freqtrade/resolvers/strategy_resolver.py @@ -167,7 +167,7 @@ class StrategyResolver(IResolver): :param extra_dir: additional directory to search for the given strategy :return: Strategy instance or None """ - if config['recursive_strategy_search']: + if 'recursive_strategy_search' in config and config['recursive_strategy_search']: extra_dirs: List[str] = [ path[0] for path in walk(f"{config['user_data_dir']}/{USERPATH_STRATEGIES}") ] # sub-directories From 64e6729ae94b0c6027dc45be48b6185f7b1dbf6a Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sun, 10 Apr 2022 18:56:28 -0600 Subject: [PATCH 037/118] docs for recursive_strategy_search --- docs/configuration.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/configuration.md b/docs/configuration.md index 2cb5dfa93..9c1b3718c 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -121,6 +121,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `order_types` | Configure order-types depending on the action (`"buy"`, `"sell"`, `"stoploss"`, `"stoploss_on_exchange"`). [More information below](#understand-order_types). [Strategy Override](#parameters-in-the-strategy).
**Datatype:** Dict | `order_time_in_force` | Configure time in force for buy and sell orders. [More information below](#understand-order_time_in_force). [Strategy Override](#parameters-in-the-strategy).
**Datatype:** Dict | `custom_price_max_distance_ratio` | Configure maximum distance ratio between current and custom entry or exit price.
*Defaults to `0.02` 2%).*
**Datatype:** Positive float +| `recursive_strategy_search` | Set to `true` to recursively search sub-directories inside `user_data/strategies` for a strategy.
**Datatype:** Boolean | `exchange.name` | **Required.** Name of the exchange class to use. [List below](#user-content-what-values-for-exchangename).
**Datatype:** String | `exchange.sandbox` | Use the 'sandbox' version of the exchange, where the exchange provides a sandbox for risk-free integration. See [here](sandbox-testing.md) in more details.
**Datatype:** Boolean | `exchange.key` | API key to use for the exchange. Only required when you are in production mode.
**Keep it in secret, do not disclose publicly.**
**Datatype:** String From fa298d6f1c1b4fd57161b14ae16259a759daa84f Mon Sep 17 00:00:00 2001 From: Italo <45588475+italodamato@users.noreply.github.com> Date: Tue, 12 Apr 2022 23:57:40 +0100 Subject: [PATCH 038/118] fix unique_list logic --- freqtrade/optimize/hyperopt.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 748cc0806..0d71e4ff5 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -426,8 +426,8 @@ class Hyperopt: for x in a_list: key = repr(x) if key not in seen: - seen.append(eval(key)) - return seen + seen.append(key) + return [eval(x) for x in seen] i = 0 asked_non_tried: List[List[Any]] = [] is_random: List[bool] = [] From 35cea6dcfa5a816cc223a966f72a1764a7c0fb93 Mon Sep 17 00:00:00 2001 From: Italo <45588475+italodamato@users.noreply.github.com> Date: Wed, 13 Apr 2022 09:36:46 +0100 Subject: [PATCH 039/118] fix unique_list --- freqtrade/optimize/hyperopt.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 0d71e4ff5..24d2b910d 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -422,12 +422,11 @@ class Hyperopt: 6. Return a list with length truncated at `n_points` ''' def unique_list(a_list): - seen = [] - for x in a_list: - key = repr(x) - if key not in seen: - seen.append(key) - return [eval(x) for x in seen] + new_list = [] + for item in a_list: + if item not in new_list: + new_list.append(item) + return new_list i = 0 asked_non_tried: List[List[Any]] = [] is_random: List[bool] = [] From 4acb77305a78940b64f1ee5550130da503dc0ced Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 13 Apr 2022 19:33:27 +0200 Subject: [PATCH 040/118] Don't break when running hyperopt-x tools on old resuts --- freqtrade/optimize/hyperopt_tools.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py index 83df7e83c..1610b3b5b 100755 --- a/freqtrade/optimize/hyperopt_tools.py +++ b/freqtrade/optimize/hyperopt_tools.py @@ -310,12 +310,15 @@ class HyperoptTools(): if not has_drawdown: # Ensure compatibility with older versions of hyperopt results trials['results_metrics.max_drawdown_account'] = None + if 'is_random' not in trials.columns: + trials['is_random'] = False # New mode, using backtest result for metrics trials['results_metrics.winsdrawslosses'] = trials.apply( lambda x: f"{x['results_metrics.wins']} {x['results_metrics.draws']:>4} " f"{x['results_metrics.losses']:>4}", axis=1) + trials = trials[['Best', 'current_epoch', 'results_metrics.total_trades', 'results_metrics.winsdrawslosses', 'results_metrics.profit_mean', 'results_metrics.profit_total_abs', From 340c0ea391f4ac89b01352ff6d4fd947715a694b Mon Sep 17 00:00:00 2001 From: Italo <45588475+italodamato@users.noreply.github.com> Date: Thu, 14 Apr 2022 14:15:11 +0100 Subject: [PATCH 041/118] update is_random before asked_non_tried is_random depends on asked_non_tried and needs to be updated first --- freqtrade/optimize/hyperopt.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 24d2b910d..babcc5491 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -438,12 +438,12 @@ class Hyperopt: else: asked = unique_list(self.opt.space.rvs(n_samples=n_points * 5)) is_random = [True for _ in range(len(asked))] - asked_non_tried += [x for x in asked - if x not in self.opt.Xi - and x not in asked_non_tried] is_random += [rand for x, rand in zip(asked, is_random) if x not in self.opt.Xi and x not in asked_non_tried] + asked_non_tried += [x for x in asked + if x not in self.opt.Xi + and x not in asked_non_tried] i += 1 if asked_non_tried: From 1153e65b3ecbb8ec97656a631890c66cf46af165 Mon Sep 17 00:00:00 2001 From: Italo <45588475+italodamato@users.noreply.github.com> Date: Thu, 14 Apr 2022 14:34:04 +0100 Subject: [PATCH 042/118] fix flake8 --- freqtrade/optimize/hyperopt_tools.py | 1 - 1 file changed, 1 deletion(-) diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py index 1610b3b5b..32a095ad8 100755 --- a/freqtrade/optimize/hyperopt_tools.py +++ b/freqtrade/optimize/hyperopt_tools.py @@ -318,7 +318,6 @@ class HyperoptTools(): lambda x: f"{x['results_metrics.wins']} {x['results_metrics.draws']:>4} " f"{x['results_metrics.losses']:>4}", axis=1) - trials = trials[['Best', 'current_epoch', 'results_metrics.total_trades', 'results_metrics.winsdrawslosses', 'results_metrics.profit_mean', 'results_metrics.profit_total_abs', From 26ba899d7d565581763d7509e5a041ad4bcb7160 Mon Sep 17 00:00:00 2001 From: froggleston Date: Sat, 16 Apr 2022 14:37:36 +0100 Subject: [PATCH 043/118] Add constant, boolean check, rename option to fit with other x_enable, check that RunMode is BACKTEST --- freqtrade/constants.py | 1 + freqtrade/optimize/backtesting.py | 10 ++++------ freqtrade/optimize/optimize_reports.py | 19 +++++++++++++++++++ 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index c6a2ab5d3..d21020a3f 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -380,6 +380,7 @@ CONF_SCHEMA = { }, 'position_adjustment_enable': {'type': 'boolean'}, 'max_entry_position_adjustment': {'type': ['integer', 'number'], 'minimum': -1}, + 'backtest_signal_candle_export_enable': {'type': 'boolean'}, }, 'definitions': { 'exchange': { diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 11704a70b..b8f63d006 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -19,7 +19,7 @@ from freqtrade.data import history from freqtrade.data.btanalysis import find_existing_backtest_stats, trade_list_to_dataframe from freqtrade.data.converter import trim_dataframe, trim_dataframes from freqtrade.data.dataprovider import DataProvider -from freqtrade.enums import BacktestState, CandleType, ExitCheckTuple, ExitType, TradingMode +from freqtrade.enums import BacktestState, CandleType, ExitCheckTuple, ExitType, TradingMode, RunMode from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from freqtrade.misc import get_strategy_run_id @@ -74,7 +74,7 @@ class Backtesting: self.strategylist: List[IStrategy] = [] self.all_results: Dict[str, Dict] = {} self.processed_dfs: Dict[str, Dict] = {} - + self._exchange_name = self.config['exchange']['name'] self.exchange = ExchangeResolver.load_exchange(self._exchange_name, self.config) self.dataprovider = DataProvider(self.config, self.exchange) @@ -129,9 +129,7 @@ class Backtesting: self.config['startup_candle_count'] = self.required_startup self.exchange.validate_required_startup_candles(self.required_startup, self.timeframe) - self.enable_backtest_signal_candle_export = False - if self.config.get('enable_backtest_signal_candle_export', None) is not None: - self.enable_backtest_signal_candle_export = bool(self.config.get('enable_backtest_signal_candle_export')) + self.backtest_signal_candle_export_enable = self.config.get('backtest_signal_candle_export_enable', False) self.trading_mode: TradingMode = config.get('trading_mode', TradingMode.SPOT) # strategies which define "can_short=True" will fail to load in Spot mode. @@ -1076,7 +1074,7 @@ class Backtesting: }) self.all_results[self.strategy.get_strategy_name()] = results - if self.enable_backtest_signal_candle_export: + if self.backtest_signal_candle_export_enable and self.dataprovider.runmode == RunMode.BACKTEST: signal_candles_only = {} for pair in preprocessed_tmp.keys(): signal_candles_only_df = DataFrame() diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index c08fa07a1..a97a6cf0f 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -44,6 +44,25 @@ def store_backtest_stats(recordfilename: Path, stats: Dict[str, DataFrame]) -> N latest_filename = Path.joinpath(filename.parent, LAST_BT_RESULT_FN) file_dump_json(latest_filename, {'latest_backtest': str(filename.name)}) +def store_backtest_signal_candles(recordfilename: Path, candles: Dict[str, Dict]) -> None: + """ + Stores backtest trade signal candles + :param recordfilename: Path object, which can either be a filename or a directory. + Filenames will be appended with a timestamp right before the suffix + while for directories, /backtest-result-_signals.pkl will be used as filename + :param stats: Dict containing the backtesting signal candles + """ + if recordfilename.is_dir(): + filename = (recordfilename / + f'backtest-result-{datetime.now().strftime("%Y-%m-%d_%H-%M-%S")}_signals.pkl') + else: + filename = Path.joinpath( + recordfilename.parent, + f'{recordfilename.stem}-{datetime.now().strftime("%Y-%m-%d_%H-%M-%S")}' + ).with_suffix(recordfilename.suffix) + + with open(filename, 'wb') as f: + pickle.dump(candles, f) def _get_line_floatfmt(stake_currency: str) -> List[str]: """ From 21734c5de77697cb75a5ebee8e52b1b9bce6b10a Mon Sep 17 00:00:00 2001 From: froggleston Date: Sat, 16 Apr 2022 14:46:30 +0100 Subject: [PATCH 044/118] Add pickle import --- freqtrade/optimize/backtesting.py | 1 + freqtrade/optimize/optimize_reports.py | 1 + 2 files changed, 2 insertions(+) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index b8f63d006..cc7dbb1fb 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -8,6 +8,7 @@ from collections import defaultdict from copy import deepcopy from datetime import datetime, timedelta, timezone from typing import Any, Dict, List, Optional, Tuple +import pickle from numpy import nan from pandas import DataFrame diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index a97a6cf0f..4265ca70d 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -3,6 +3,7 @@ from copy import deepcopy from datetime import datetime, timedelta, timezone from pathlib import Path from typing import Any, Dict, List, Union +import pickle from numpy import int64 from pandas import DataFrame, to_datetime From 8990ba27099357c7f63540fd05e7c7db98961270 Mon Sep 17 00:00:00 2001 From: froggleston Date: Sat, 16 Apr 2022 14:49:53 +0100 Subject: [PATCH 045/118] Fix store signal candles --- freqtrade/optimize/backtesting.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index cc7dbb1fb..4ed3f85be 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -27,7 +27,7 @@ from freqtrade.misc import get_strategy_run_id from freqtrade.mixins import LoggingMixin from freqtrade.optimize.bt_progress import BTProgress from freqtrade.optimize.optimize_reports import (generate_backtest_stats, show_backtest_results, - store_backtest_stats) + store_backtest_stats, store_backtest_signal_candles) from freqtrade.persistence import LocalTrade, Order, PairLocks, Trade from freqtrade.plugins.pairlistmanager import PairListManager from freqtrade.plugins.protectionmanager import ProtectionManager @@ -1157,6 +1157,9 @@ class Backtesting: if self.config.get('export', 'none') == 'trades': store_backtest_stats(self.config['exportfilename'], self.results) + if self.enable_backtest_signal_candle_export and self.dataprovider.runmode == RunMode.BACKTEST: + store_backtest_signal_candles(self.config['exportfilename'], self.processed_dfs) + # Results may be mixed up now. Sort them so they follow --strategy-list order. if 'strategy_list' in self.config and len(self.results) > 0: self.results['strategy_comparison'] = sorted( From b1bcf9f33c8157fc1e62fa67532151afaaacb8f2 Mon Sep 17 00:00:00 2001 From: froggleston Date: Sat, 16 Apr 2022 14:58:17 +0100 Subject: [PATCH 046/118] Fix backtest_enable typo --- freqtrade/optimize/backtesting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 4ed3f85be..f19cd488e 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -1157,7 +1157,7 @@ class Backtesting: if self.config.get('export', 'none') == 'trades': store_backtest_stats(self.config['exportfilename'], self.results) - if self.enable_backtest_signal_candle_export and self.dataprovider.runmode == RunMode.BACKTEST: + if self.backtest_signal_candle_export_enable and self.dataprovider.runmode == RunMode.BACKTEST: store_backtest_signal_candles(self.config['exportfilename'], self.processed_dfs) # Results may be mixed up now. Sort them so they follow --strategy-list order. From f55a9940a7e812f07437e4a207652b8152d7d131 Mon Sep 17 00:00:00 2001 From: froggleston Date: Sat, 16 Apr 2022 16:15:04 +0100 Subject: [PATCH 047/118] Fix line spacing --- freqtrade/optimize/optimize_reports.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 4265ca70d..f870bd1f5 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -45,6 +45,7 @@ def store_backtest_stats(recordfilename: Path, stats: Dict[str, DataFrame]) -> N latest_filename = Path.joinpath(filename.parent, LAST_BT_RESULT_FN) file_dump_json(latest_filename, {'latest_backtest': str(filename.name)}) + def store_backtest_signal_candles(recordfilename: Path, candles: Dict[str, Dict]) -> None: """ Stores backtest trade signal candles @@ -65,6 +66,7 @@ def store_backtest_signal_candles(recordfilename: Path, candles: Dict[str, Dict] with open(filename, 'wb') as f: pickle.dump(candles, f) + def _get_line_floatfmt(stake_currency: str) -> List[str]: """ Generate floatformat (goes in line with _generate_result_line()) From a63affc5f14156b671c805de9256d3e611d05c93 Mon Sep 17 00:00:00 2001 From: froggleston Date: Sat, 16 Apr 2022 16:32:04 +0100 Subject: [PATCH 048/118] Fix flake8 complaints --- freqtrade/optimize/backtesting.py | 13 ++++++++----- freqtrade/optimize/optimize_reports.py | 3 ++- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index f19cd488e..7e19e26e4 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -8,7 +8,6 @@ from collections import defaultdict from copy import deepcopy from datetime import datetime, timedelta, timezone from typing import Any, Dict, List, Optional, Tuple -import pickle from numpy import nan from pandas import DataFrame @@ -20,14 +19,16 @@ from freqtrade.data import history from freqtrade.data.btanalysis import find_existing_backtest_stats, trade_list_to_dataframe from freqtrade.data.converter import trim_dataframe, trim_dataframes from freqtrade.data.dataprovider import DataProvider -from freqtrade.enums import BacktestState, CandleType, ExitCheckTuple, ExitType, TradingMode, RunMode +from freqtrade.enums import (BacktestState, CandleType, ExitCheckTuple, ExitType, TradingMode, + RunMode) from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from freqtrade.misc import get_strategy_run_id from freqtrade.mixins import LoggingMixin from freqtrade.optimize.bt_progress import BTProgress from freqtrade.optimize.optimize_reports import (generate_backtest_stats, show_backtest_results, - store_backtest_stats, store_backtest_signal_candles) + store_backtest_stats, + store_backtest_signal_candles) from freqtrade.persistence import LocalTrade, Order, PairLocks, Trade from freqtrade.plugins.pairlistmanager import PairListManager from freqtrade.plugins.protectionmanager import ProtectionManager @@ -1075,7 +1076,8 @@ class Backtesting: }) self.all_results[self.strategy.get_strategy_name()] = results - if self.backtest_signal_candle_export_enable and self.dataprovider.runmode == RunMode.BACKTEST: + if self.backtest_signal_candle_export_enable and + self.dataprovider.runmode == RunMode.BACKTEST: signal_candles_only = {} for pair in preprocessed_tmp.keys(): signal_candles_only_df = DataFrame() @@ -1157,7 +1159,8 @@ class Backtesting: if self.config.get('export', 'none') == 'trades': store_backtest_stats(self.config['exportfilename'], self.results) - if self.backtest_signal_candle_export_enable and self.dataprovider.runmode == RunMode.BACKTEST: + if self.backtest_signal_candle_export_enable and + self.dataprovider.runmode == RunMode.BACKTEST: store_backtest_signal_candles(self.config['exportfilename'], self.processed_dfs) # Results may be mixed up now. Sort them so they follow --strategy-list order. diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index f870bd1f5..06b393b60 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -51,7 +51,8 @@ def store_backtest_signal_candles(recordfilename: Path, candles: Dict[str, Dict] Stores backtest trade signal candles :param recordfilename: Path object, which can either be a filename or a directory. Filenames will be appended with a timestamp right before the suffix - while for directories, /backtest-result-_signals.pkl will be used as filename + while for directories, /backtest-result-_signals.pkl will be used + as filename :param stats: Dict containing the backtesting signal candles """ if recordfilename.is_dir(): From 7210a1173074915b76751085ff3df2f23b2a55aa Mon Sep 17 00:00:00 2001 From: froggleston Date: Sat, 16 Apr 2022 16:37:06 +0100 Subject: [PATCH 049/118] Fix flake8 complaints --- freqtrade/optimize/backtesting.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 7e19e26e4..839463218 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -131,7 +131,8 @@ class Backtesting: self.config['startup_candle_count'] = self.required_startup self.exchange.validate_required_startup_candles(self.required_startup, self.timeframe) - self.backtest_signal_candle_export_enable = self.config.get('backtest_signal_candle_export_enable', False) + self.backtest_signal_candle_export_enable = self.config.get( + 'backtest_signal_candle_export_enable', False) self.trading_mode: TradingMode = config.get('trading_mode', TradingMode.SPOT) # strategies which define "can_short=True" will fail to load in Spot mode. From b738c4e695e69d51a3827f3f99b42e40f0b01846 Mon Sep 17 00:00:00 2001 From: froggleston Date: Sat, 16 Apr 2022 16:49:20 +0100 Subject: [PATCH 050/118] Fix flake8 complaints --- freqtrade/optimize/backtesting.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 839463218..e9440f62c 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -20,7 +20,7 @@ from freqtrade.data.btanalysis import find_existing_backtest_stats, trade_list_t from freqtrade.data.converter import trim_dataframe, trim_dataframes from freqtrade.data.dataprovider import DataProvider from freqtrade.enums import (BacktestState, CandleType, ExitCheckTuple, ExitType, TradingMode, - RunMode) + RunMode) from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from freqtrade.misc import get_strategy_run_id @@ -1077,8 +1077,8 @@ class Backtesting: }) self.all_results[self.strategy.get_strategy_name()] = results - if self.backtest_signal_candle_export_enable and - self.dataprovider.runmode == RunMode.BACKTEST: + if self.backtest_signal_candle_export_enable and \ + self.dataprovider.runmode == RunMode.BACKTEST: signal_candles_only = {} for pair in preprocessed_tmp.keys(): signal_candles_only_df = DataFrame() @@ -1160,8 +1160,8 @@ class Backtesting: if self.config.get('export', 'none') == 'trades': store_backtest_stats(self.config['exportfilename'], self.results) - if self.backtest_signal_candle_export_enable and - self.dataprovider.runmode == RunMode.BACKTEST: + if self.backtest_signal_candle_export_enable and \ + self.dataprovider.runmode == RunMode.BACKTEST: store_backtest_signal_candles(self.config['exportfilename'], self.processed_dfs) # Results may be mixed up now. Sort them so they follow --strategy-list order. From 34fb8dacd7fb9083461ad2986b47d2d8480a7e05 Mon Sep 17 00:00:00 2001 From: froggleston Date: Sat, 16 Apr 2022 17:03:24 +0100 Subject: [PATCH 051/118] Fix isort complaints --- freqtrade/optimize/backtesting.py | 8 ++++---- freqtrade/optimize/optimize_reports.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index e9440f62c..03a6cade0 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -19,16 +19,16 @@ from freqtrade.data import history from freqtrade.data.btanalysis import find_existing_backtest_stats, trade_list_to_dataframe from freqtrade.data.converter import trim_dataframe, trim_dataframes from freqtrade.data.dataprovider import DataProvider -from freqtrade.enums import (BacktestState, CandleType, ExitCheckTuple, ExitType, TradingMode, - RunMode) +from freqtrade.enums import (BacktestState, CandleType, ExitCheckTuple, ExitType, RunMode, + TradingMode) from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from freqtrade.misc import get_strategy_run_id from freqtrade.mixins import LoggingMixin from freqtrade.optimize.bt_progress import BTProgress from freqtrade.optimize.optimize_reports import (generate_backtest_stats, show_backtest_results, - store_backtest_stats, - store_backtest_signal_candles) + store_backtest_signal_candles, + store_backtest_stats) from freqtrade.persistence import LocalTrade, Order, PairLocks, Trade from freqtrade.plugins.pairlistmanager import PairListManager from freqtrade.plugins.protectionmanager import ProtectionManager diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 06b393b60..f0b2e2e71 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -1,9 +1,9 @@ import logging +import pickle from copy import deepcopy from datetime import datetime, timedelta, timezone from pathlib import Path from typing import Any, Dict, List, Union -import pickle from numpy import int64 from pandas import DataFrame, to_datetime From 84f486295d806d4ae8347582f7d3d803bf41d256 Mon Sep 17 00:00:00 2001 From: froggleston Date: Tue, 19 Apr 2022 12:48:21 +0100 Subject: [PATCH 052/118] Add tests for new storing of backtest signal candles --- freqtrade/misc.py | 16 ++++++++++++++++ freqtrade/optimize/optimize_reports.py | 10 ++++------ tests/optimize/test_optimize_reports.py | 24 +++++++++++++++++++++++- 3 files changed, 43 insertions(+), 7 deletions(-) diff --git a/freqtrade/misc.py b/freqtrade/misc.py index d5572ea0b..9087ec6e2 100644 --- a/freqtrade/misc.py +++ b/freqtrade/misc.py @@ -4,6 +4,7 @@ Various tool function for Freqtrade and scripts import gzip import hashlib import logging +import pickle import re from copy import deepcopy from datetime import datetime @@ -86,6 +87,21 @@ def file_dump_json(filename: Path, data: Any, is_zip: bool = False, log: bool = logger.debug(f'done json to "{filename}"') +def file_dump_pickle(filename: Path, data: Any, log: bool = True) -> None: + """ + Dump object data into a file + :param filename: file to create + :param data: Object data to save + :return: + """ + + if log: + logger.info(f'dumping pickle to "{filename}"') + with open(filename, 'wb') as fp: + pickle.dump(data, fp) + logger.debug(f'done pickling to "{filename}"') + + def json_load(datafile: IO) -> Any: """ load data with rapidjson diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index f0b2e2e71..ed29af839 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -1,5 +1,4 @@ import logging -import pickle from copy import deepcopy from datetime import datetime, timedelta, timezone from pathlib import Path @@ -12,8 +11,8 @@ from tabulate import tabulate from freqtrade.constants import DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN, UNLIMITED_STAKE_AMOUNT from freqtrade.data.btanalysis import (calculate_csum, calculate_market_change, calculate_max_drawdown) -from freqtrade.misc import (decimals_per_coin, file_dump_json, get_backtest_metadata_filename, - round_coin_value) +from freqtrade.misc import (decimals_per_coin, file_dump_json, file_dump_pickle, + get_backtest_metadata_filename, round_coin_value) logger = logging.getLogger(__name__) @@ -61,11 +60,10 @@ def store_backtest_signal_candles(recordfilename: Path, candles: Dict[str, Dict] else: filename = Path.joinpath( recordfilename.parent, - f'{recordfilename.stem}-{datetime.now().strftime("%Y-%m-%d_%H-%M-%S")}' + f'{recordfilename.stem}-{datetime.now().strftime("%Y-%m-%d_%H-%M-%S")}_signals' ).with_suffix(recordfilename.suffix) - with open(filename, 'wb') as f: - pickle.dump(candles, f) + file_dump_pickle(filename, candles) def _get_line_floatfmt(stake_currency: str) -> List[str]: diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index 05c0bf575..a09620a71 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -1,5 +1,5 @@ import re -from datetime import timedelta +from datetime import timedelta, timezone from pathlib import Path import pandas as pd @@ -19,6 +19,7 @@ from freqtrade.optimize.optimize_reports import (_get_resample_from_period, gene generate_periodic_breakdown_stats, generate_strategy_comparison, generate_trading_stats, show_sorted_pairlist, + store_backtest_signal_candles, store_backtest_stats, text_table_bt_results, text_table_exit_reason, text_table_strategy) from freqtrade.resolvers.strategy_resolver import StrategyResolver @@ -201,6 +202,27 @@ def test_store_backtest_stats(testdatadir, mocker): assert str(dump_mock.call_args_list[0][0][0]).startswith(str(testdatadir / 'testresult')) +def test_store_backtest_candles(testdatadir, mocker): + + dump_mock = mocker.patch('freqtrade.optimize.optimize_reports.file_dump_pickle') + + # test directory exporting + store_backtest_signal_candles(testdatadir, {'DefStrat': {'UNITTEST/BTC': pd.DataFrame()}}) + + assert dump_mock.call_count == 1 + assert isinstance(dump_mock.call_args_list[0][0][0], Path) + assert str(dump_mock.call_args_list[0][0][0]).endswith(str('_signals.pkl')) + + dump_mock.reset_mock() + # test file exporting + filename = testdatadir / 'testresult' + store_backtest_signal_candles(filename, {'DefStrat': {'UNITTEST/BTC': pd.DataFrame()}}) + assert dump_mock.call_count == 1 + assert isinstance(dump_mock.call_args_list[0][0][0], Path) + # result will be testdatadir / testresult-_signals.pkl + assert str(dump_mock.call_args_list[0][0][0]).endswith(str('_signals.pkl')) + + def test_generate_pair_metrics(): results = pd.DataFrame( From aa5984930d3cb19b7fc2ed82b5590a6bad640d42 Mon Sep 17 00:00:00 2001 From: froggleston Date: Tue, 19 Apr 2022 13:00:09 +0100 Subject: [PATCH 053/118] Fix filename generation --- freqtrade/optimize/optimize_reports.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index ed29af839..05eec693e 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -60,8 +60,8 @@ def store_backtest_signal_candles(recordfilename: Path, candles: Dict[str, Dict] else: filename = Path.joinpath( recordfilename.parent, - f'{recordfilename.stem}-{datetime.now().strftime("%Y-%m-%d_%H-%M-%S")}_signals' - ).with_suffix(recordfilename.suffix) + f'{recordfilename.stem}-{datetime.now().strftime("%Y-%m-%d_%H-%M-%S")}_signals.pkl' + ) file_dump_pickle(filename, candles) From 3ad1411f5ecba585be2a23972d412f692da15018 Mon Sep 17 00:00:00 2001 From: froggleston Date: Tue, 19 Apr 2022 13:08:01 +0100 Subject: [PATCH 054/118] Fix imports --- tests/optimize/test_optimize_reports.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index a09620a71..d72cf4e86 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -1,5 +1,5 @@ import re -from datetime import timedelta, timezone +from datetime import timedelta from pathlib import Path import pandas as pd From 9421d19cba64813a16094ff3bf2d332641e3fd89 Mon Sep 17 00:00:00 2001 From: froggleston Date: Tue, 19 Apr 2022 14:05:03 +0100 Subject: [PATCH 055/118] Add documentation --- docs/configuration.md | 7 +-- docs/strategy_analysis_example.md | 74 ++++++++++++++++++++++++++++++- 2 files changed, 77 insertions(+), 4 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 369c4e2dd..061b6c77c 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -11,7 +11,7 @@ Per default, the bot loads the configuration from the `config.json` file, locate You can specify a different configuration file used by the bot with the `-c/--config` command-line option. -If you used the [Quick start](installation.md/#quick-start) method for installing +If you used the [Quick start](installation.md/#quick-start) method for installing the bot, the installation script should have already created the default configuration file (`config.json`) for you. If the default configuration file is not created we recommend to use `freqtrade new-config --config config.json` to generate a basic configuration file. @@ -64,7 +64,7 @@ This is similar to using multiple `--config` parameters, but simpler in usage as "config-private.json" ] ``` - + ``` bash freqtrade trade --config user_data/config.json <...> ``` @@ -100,7 +100,7 @@ This is similar to using multiple `--config` parameters, but simpler in usage as "stake_amount": "unlimited", } ``` - + Resulting combined configuration: ``` json title="Result" @@ -229,6 +229,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `dataformat_trades` | Data format to use to store historical trades data.
*Defaults to `jsongz`*.
**Datatype:** String | `position_adjustment_enable` | Enables the strategy to use position adjustments (additional buys or sells). [More information here](strategy-callbacks.md#adjust-trade-position).
[Strategy Override](#parameters-in-the-strategy).
*Defaults to `false`.*
**Datatype:** Boolean | `max_entry_position_adjustment` | Maximum additional order(s) for each open trade on top of the first entry Order. Set it to `-1` for unlimited additional orders. [More information here](strategy-callbacks.md#adjust-trade-position).
[Strategy Override](#parameters-in-the-strategy).
*Defaults to `-1`.*
**Datatype:** Positive Integer or -1 +| `backtest_signal_candle_export_enable` | Enables the exporting of signal candles for use in post-backtesting analysis of buy tags. See [Strategy Analysis](strategy_analysis_example.md#analyse-the-buy-entry-and-sell-exit-tags).
*Defaults to `false`.*
**Datatype:** Boolean ### Parameters in the strategy diff --git a/docs/strategy_analysis_example.md b/docs/strategy_analysis_example.md index 2fa84a6df..48f54c824 100644 --- a/docs/strategy_analysis_example.md +++ b/docs/strategy_analysis_example.md @@ -93,7 +93,7 @@ from freqtrade.data.btanalysis import load_backtest_data, load_backtest_stats # if backtest_dir points to a directory, it'll automatically load the last backtest file. backtest_dir = config["user_data_dir"] / "backtest_results" -# backtest_dir can also point to a specific file +# backtest_dir can also point to a specific file # backtest_dir = config["user_data_dir"] / "backtest_results/backtest-result-2020-07-01_20-04-22.json" ``` @@ -250,3 +250,75 @@ fig.show() ``` Feel free to submit an issue or Pull Request enhancing this document if you would like to share ideas on how to best analyze the data. + +## Analyse the buy/entry and sell/exit tags + +It can be helpful to understand how a strategy behaves according to the buy/entry tags used to +mark up different buy conditions. You might want to see more complex statistics about each buy and +sell condition above those provided by the default backtesting output. You may also want to +determine indicator values on the signal candle that resulted in a trade opening. + +We first need to enable the exporting of trades from backtesting: + +``` +freqtrade backtesting -c --timeframe --strategy --timerange= --export=trades --export-filename=user_data/backtest_results/- +``` + +To analyse the buy tags, we need to use the buy_reasons.py script in the `scripts/` +folder. We need the signal candles for each opened trade so add the following option to your +config file: + +``` +'backtest_signal_candle_export_enable': true, +``` + +This will tell freqtrade to output a pickled dictionary of strategy, pairs and corresponding +DataFrame of the candles that resulted in buy signals. Depending on how many buys your strategy +makes, this file may get quite large, so periodically check your `user_data/backtest_results` +folder to delete old exports. + +Before running your next backtest, make sure you either delete your old backtest results or run +backtesting with the `--cache none` option to make sure no cached results are used. + +If all goes well, you should now see a `backtest-result-{timestamp}_signals.pkl` file in the +`user_data/backtest_results` folder. + +Now run the buy_reasons.py script, supplying a few options: + +``` +./scripts/buy_reasons.py -c -s -t -g0,1,2,3,4 +``` + +The `-g` option is used to specify the various tabular outputs, ranging from the simplest (0) +to the most detailed per pair, per buy and per sell tag (4). More options are available by +running with the `-h` option. + +### Tuning the buy tags and sell tags to display + +To show only certain buy and sell tags in the displayed output, use the following two options: + +``` +--buy_reason_list : Comma separated list of buy signals to analyse. Default: "all" +--sell_reason_list : Comma separated list of sell signals to analyse. Default: "stop_loss,trailing_stop_loss" +``` + +For example: + +``` +./scripts/buy_reasons.py -c -s -t -g0,1,2,3,4 --buy_reason_list "buy_tag_a,buy_tag_b" --sell_reason_list "roi,custom_sell_tag_a,stop_loss" +``` + +### Outputting signal candle indicators + +The real power of the buy_reasons.py script comes from the ability to print out the indicator +values present on signal candles to allow fine-grained investigation and tuning of buy signal +indicators. To print out a column for a given set of indicators, use the `--indicator-list` +option: + +``` +./scripts/buy_reasons.py -c -s -t -g0,1,2,3,4 --buy_reason_list "buy_tag_a,buy_tag_b" --sell_reason_list "roi,custom_sell_tag_a,stop_loss" --indicator_list "rsi,rsi_1h,bb_lowerband,ema_9,macd,macdsignal" +``` + +The indicators have to be present in your strategy's main dataframe (either for your main +timeframe or for informatives) otherwise they will simply be ignored in the script +output. From b3cb7226467c359b0748cb4866f8f8fcfa649a53 Mon Sep 17 00:00:00 2001 From: froggleston Date: Wed, 20 Apr 2022 13:38:52 +0100 Subject: [PATCH 056/118] Use joblib instead of pickle, add signal candle read/write test, move docs to new Advanced Backtesting doc --- docs/advanced-backtesting.md | 75 +++++++++++++++++++++++++ docs/data-analysis.md | 1 + docs/strategy_analysis_example.md | 72 ------------------------ freqtrade/misc.py | 10 ++-- freqtrade/optimize/optimize_reports.py | 8 ++- tests/optimize/test_optimize_reports.py | 48 ++++++++++++++-- 6 files changed, 128 insertions(+), 86 deletions(-) create mode 100644 docs/advanced-backtesting.md diff --git a/docs/advanced-backtesting.md b/docs/advanced-backtesting.md new file mode 100644 index 000000000..9a7d71767 --- /dev/null +++ b/docs/advanced-backtesting.md @@ -0,0 +1,75 @@ +# Advanced Backtesting Analysis + +## Analyse the buy/entry and sell/exit tags + +It can be helpful to understand how a strategy behaves according to the buy/entry tags used to +mark up different buy conditions. You might want to see more complex statistics about each buy and +sell condition above those provided by the default backtesting output. You may also want to +determine indicator values on the signal candle that resulted in a trade opening. + +!!! Note + The following buy reason analysis is only available for backtesting, *not hyperopt*. + +We first need to enable the exporting of trades from backtesting: + +```bash +freqtrade backtesting -c --timeframe --strategy --timerange= --export=trades --export-filename=user_data/backtest_results/- +``` + +To analyse the buy tags, we need to use the `freqtrade tag-analysis` command. We need the signal +candles for each opened trade so add the following option to your config file: + +``` +'backtest_signal_candle_export_enable': true, +``` + +This will tell freqtrade to output a pickled dictionary of strategy, pairs and corresponding +DataFrame of the candles that resulted in buy signals. Depending on how many buys your strategy +makes, this file may get quite large, so periodically check your `user_data/backtest_results` +folder to delete old exports. + +Before running your next backtest, make sure you either delete your old backtest results or run +backtesting with the `--cache none` option to make sure no cached results are used. + +If all goes well, you should now see a `backtest-result-{timestamp}_signals.pkl` file in the +`user_data/backtest_results` folder. + +Now run the buy_reasons.py script, supplying a few options: + +```bash +freqtrade tag-analysis -c -s -t -g0,1,2,3,4 +``` + +The `-g` option is used to specify the various tabular outputs, ranging from the simplest (0) +to the most detailed per pair, per buy and per sell tag (4). More options are available by +running with the `-h` option. + +### Tuning the buy tags and sell tags to display + +To show only certain buy and sell tags in the displayed output, use the following two options: + +``` +--buy_reason_list : Comma separated list of buy signals to analyse. Default: "all" +--sell_reason_list : Comma separated list of sell signals to analyse. Default: "stop_loss,trailing_stop_loss" +``` + +For example: + +```bash +freqtrade tag-analysis -c -s -t -g0,1,2,3,4 --buy_reason_list "buy_tag_a,buy_tag_b" --sell_reason_list "roi,custom_sell_tag_a,stop_loss" +``` + +### Outputting signal candle indicators + +The real power of the buy_reasons.py script comes from the ability to print out the indicator +values present on signal candles to allow fine-grained investigation and tuning of buy signal +indicators. To print out a column for a given set of indicators, use the `--indicator-list` +option: + +```bash +freqtrade tag-analysis -c -s -t -g0,1,2,3,4 --buy_reason_list "buy_tag_a,buy_tag_b" --sell_reason_list "roi,custom_sell_tag_a,stop_loss" --indicator_list "rsi,rsi_1h,bb_lowerband,ema_9,macd,macdsignal" +``` + +The indicators have to be present in your strategy's main DataFrame (either for your main +timeframe or for informatives) otherwise they will simply be ignored in the script +output. diff --git a/docs/data-analysis.md b/docs/data-analysis.md index 9a79ee5ed..926ed3eae 100644 --- a/docs/data-analysis.md +++ b/docs/data-analysis.md @@ -122,5 +122,6 @@ Best avoid relative paths, since this starts at the storage location of the jupy * [Strategy debugging](strategy_analysis_example.md) - also available as Jupyter notebook (`user_data/notebooks/strategy_analysis_example.ipynb`) * [Plotting](plotting.md) +* [Tag Analysis](advanced-backtesting.md) Feel free to submit an issue or Pull Request enhancing this document if you would like to share ideas on how to best analyze the data. diff --git a/docs/strategy_analysis_example.md b/docs/strategy_analysis_example.md index 48f54c824..ae0c6a6a3 100644 --- a/docs/strategy_analysis_example.md +++ b/docs/strategy_analysis_example.md @@ -250,75 +250,3 @@ fig.show() ``` Feel free to submit an issue or Pull Request enhancing this document if you would like to share ideas on how to best analyze the data. - -## Analyse the buy/entry and sell/exit tags - -It can be helpful to understand how a strategy behaves according to the buy/entry tags used to -mark up different buy conditions. You might want to see more complex statistics about each buy and -sell condition above those provided by the default backtesting output. You may also want to -determine indicator values on the signal candle that resulted in a trade opening. - -We first need to enable the exporting of trades from backtesting: - -``` -freqtrade backtesting -c --timeframe --strategy --timerange= --export=trades --export-filename=user_data/backtest_results/- -``` - -To analyse the buy tags, we need to use the buy_reasons.py script in the `scripts/` -folder. We need the signal candles for each opened trade so add the following option to your -config file: - -``` -'backtest_signal_candle_export_enable': true, -``` - -This will tell freqtrade to output a pickled dictionary of strategy, pairs and corresponding -DataFrame of the candles that resulted in buy signals. Depending on how many buys your strategy -makes, this file may get quite large, so periodically check your `user_data/backtest_results` -folder to delete old exports. - -Before running your next backtest, make sure you either delete your old backtest results or run -backtesting with the `--cache none` option to make sure no cached results are used. - -If all goes well, you should now see a `backtest-result-{timestamp}_signals.pkl` file in the -`user_data/backtest_results` folder. - -Now run the buy_reasons.py script, supplying a few options: - -``` -./scripts/buy_reasons.py -c -s -t -g0,1,2,3,4 -``` - -The `-g` option is used to specify the various tabular outputs, ranging from the simplest (0) -to the most detailed per pair, per buy and per sell tag (4). More options are available by -running with the `-h` option. - -### Tuning the buy tags and sell tags to display - -To show only certain buy and sell tags in the displayed output, use the following two options: - -``` ---buy_reason_list : Comma separated list of buy signals to analyse. Default: "all" ---sell_reason_list : Comma separated list of sell signals to analyse. Default: "stop_loss,trailing_stop_loss" -``` - -For example: - -``` -./scripts/buy_reasons.py -c -s -t -g0,1,2,3,4 --buy_reason_list "buy_tag_a,buy_tag_b" --sell_reason_list "roi,custom_sell_tag_a,stop_loss" -``` - -### Outputting signal candle indicators - -The real power of the buy_reasons.py script comes from the ability to print out the indicator -values present on signal candles to allow fine-grained investigation and tuning of buy signal -indicators. To print out a column for a given set of indicators, use the `--indicator-list` -option: - -``` -./scripts/buy_reasons.py -c -s -t -g0,1,2,3,4 --buy_reason_list "buy_tag_a,buy_tag_b" --sell_reason_list "roi,custom_sell_tag_a,stop_loss" --indicator_list "rsi,rsi_1h,bb_lowerband,ema_9,macd,macdsignal" -``` - -The indicators have to be present in your strategy's main dataframe (either for your main -timeframe or for informatives) otherwise they will simply be ignored in the script -output. diff --git a/freqtrade/misc.py b/freqtrade/misc.py index 9087ec6e2..be12d8224 100644 --- a/freqtrade/misc.py +++ b/freqtrade/misc.py @@ -4,7 +4,6 @@ Various tool function for Freqtrade and scripts import gzip import hashlib import logging -import pickle import re from copy import deepcopy from datetime import datetime @@ -13,6 +12,7 @@ from typing import Any, Iterator, List, Union from typing.io import IO from urllib.parse import urlparse +import joblib import rapidjson from freqtrade.constants import DECIMAL_PER_COIN_FALLBACK, DECIMALS_PER_COIN @@ -87,7 +87,7 @@ def file_dump_json(filename: Path, data: Any, is_zip: bool = False, log: bool = logger.debug(f'done json to "{filename}"') -def file_dump_pickle(filename: Path, data: Any, log: bool = True) -> None: +def file_dump_joblib(filename: Path, data: Any, log: bool = True) -> None: """ Dump object data into a file :param filename: file to create @@ -96,10 +96,10 @@ def file_dump_pickle(filename: Path, data: Any, log: bool = True) -> None: """ if log: - logger.info(f'dumping pickle to "{filename}"') + logger.info(f'dumping joblib to "{filename}"') with open(filename, 'wb') as fp: - pickle.dump(data, fp) - logger.debug(f'done pickling to "{filename}"') + joblib.dump(data, fp) + logger.debug(f'done joblib dump to "{filename}"') def json_load(datafile: IO) -> Any: diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 05eec693e..6288ee16a 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -11,7 +11,7 @@ from tabulate import tabulate from freqtrade.constants import DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN, UNLIMITED_STAKE_AMOUNT from freqtrade.data.btanalysis import (calculate_csum, calculate_market_change, calculate_max_drawdown) -from freqtrade.misc import (decimals_per_coin, file_dump_json, file_dump_pickle, +from freqtrade.misc import (decimals_per_coin, file_dump_joblib, file_dump_json, get_backtest_metadata_filename, round_coin_value) @@ -45,7 +45,7 @@ def store_backtest_stats(recordfilename: Path, stats: Dict[str, DataFrame]) -> N file_dump_json(latest_filename, {'latest_backtest': str(filename.name)}) -def store_backtest_signal_candles(recordfilename: Path, candles: Dict[str, Dict]) -> None: +def store_backtest_signal_candles(recordfilename: Path, candles: Dict[str, Dict]) -> Path: """ Stores backtest trade signal candles :param recordfilename: Path object, which can either be a filename or a directory. @@ -63,7 +63,9 @@ def store_backtest_signal_candles(recordfilename: Path, candles: Dict[str, Dict] f'{recordfilename.stem}-{datetime.now().strftime("%Y-%m-%d_%H-%M-%S")}_signals.pkl' ) - file_dump_pickle(filename, candles) + file_dump_joblib(filename, candles) + + return filename def _get_line_floatfmt(stake_currency: str) -> List[str]: diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index d72cf4e86..ff8d420b3 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -2,6 +2,7 @@ import re from datetime import timedelta from pathlib import Path +import joblib import pandas as pd import pytest from arrow import Arrow @@ -204,23 +205,58 @@ def test_store_backtest_stats(testdatadir, mocker): def test_store_backtest_candles(testdatadir, mocker): - dump_mock = mocker.patch('freqtrade.optimize.optimize_reports.file_dump_pickle') + dump_mock = mocker.patch('freqtrade.optimize.optimize_reports.file_dump_joblib') - # test directory exporting - store_backtest_signal_candles(testdatadir, {'DefStrat': {'UNITTEST/BTC': pd.DataFrame()}}) + candle_dict = {'DefStrat': {'UNITTEST/BTC': pd.DataFrame()}} + + # mock directory exporting + store_backtest_signal_candles(testdatadir, candle_dict) assert dump_mock.call_count == 1 assert isinstance(dump_mock.call_args_list[0][0][0], Path) assert str(dump_mock.call_args_list[0][0][0]).endswith(str('_signals.pkl')) dump_mock.reset_mock() - # test file exporting - filename = testdatadir / 'testresult' - store_backtest_signal_candles(filename, {'DefStrat': {'UNITTEST/BTC': pd.DataFrame()}}) + # mock file exporting + filename = Path(testdatadir / 'testresult') + store_backtest_signal_candles(filename, candle_dict) assert dump_mock.call_count == 1 assert isinstance(dump_mock.call_args_list[0][0][0], Path) # result will be testdatadir / testresult-_signals.pkl assert str(dump_mock.call_args_list[0][0][0]).endswith(str('_signals.pkl')) + dump_mock.reset_mock() + + +def test_write_read_backtest_candles(tmpdir): + + candle_dict = {'DefStrat': {'UNITTEST/BTC': pd.DataFrame()}} + + # test directory exporting + stored_file = store_backtest_signal_candles(Path(tmpdir), candle_dict) + scp = open(stored_file, "rb") + pickled_signal_candles = joblib.load(scp) + scp.close() + + assert pickled_signal_candles.keys() == candle_dict.keys() + assert pickled_signal_candles['DefStrat'].keys() == pickled_signal_candles['DefStrat'].keys() + assert pickled_signal_candles['DefStrat']['UNITTEST/BTC'] \ + .equals(pickled_signal_candles['DefStrat']['UNITTEST/BTC']) + + _clean_test_file(stored_file) + + # test file exporting + filename = Path(tmpdir / 'testresult') + stored_file = store_backtest_signal_candles(filename, candle_dict) + scp = open(stored_file, "rb") + pickled_signal_candles = joblib.load(scp) + scp.close() + + assert pickled_signal_candles.keys() == candle_dict.keys() + assert pickled_signal_candles['DefStrat'].keys() == pickled_signal_candles['DefStrat'].keys() + assert pickled_signal_candles['DefStrat']['UNITTEST/BTC'] \ + .equals(pickled_signal_candles['DefStrat']['UNITTEST/BTC']) + + _clean_test_file(stored_file) def test_generate_pair_metrics(): From ea7fb4e6e6182c71aabcaf0fea9bf52214644b90 Mon Sep 17 00:00:00 2001 From: froggleston Date: Wed, 20 Apr 2022 13:51:45 +0100 Subject: [PATCH 057/118] Revert docs to buy_reasons script version --- docs/advanced-backtesting.md | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/docs/advanced-backtesting.md b/docs/advanced-backtesting.md index 9a7d71767..d8c6c505e 100644 --- a/docs/advanced-backtesting.md +++ b/docs/advanced-backtesting.md @@ -13,11 +13,15 @@ determine indicator values on the signal candle that resulted in a trade opening We first need to enable the exporting of trades from backtesting: ```bash -freqtrade backtesting -c --timeframe --strategy --timerange= --export=trades --export-filename=user_data/backtest_results/- +freqtrade backtesting -c --timeframe --strategy --timerange= --export=trades ``` -To analyse the buy tags, we need to use the `freqtrade tag-analysis` command. We need the signal -candles for each opened trade so add the following option to your config file: +To analyse the buy tags, we need to use the `buy_reasons.py` script from +[froggleston's repo](https://github.com/froggleston/freqtrade-buyreasons). Follow the instructions +in their README to copy the script into your `freqtrade/scripts/` folder. + +We then need the signal candles for each opened trade so add the following option to your +config file: ``` 'backtest_signal_candle_export_enable': true, @@ -37,7 +41,7 @@ If all goes well, you should now see a `backtest-result-{timestamp}_signals.pkl` Now run the buy_reasons.py script, supplying a few options: ```bash -freqtrade tag-analysis -c -s -t -g0,1,2,3,4 +python3 scripts/buy_reasons.py -c -s -t -g0,1,2,3,4 ``` The `-g` option is used to specify the various tabular outputs, ranging from the simplest (0) @@ -56,7 +60,7 @@ To show only certain buy and sell tags in the displayed output, use the followin For example: ```bash -freqtrade tag-analysis -c -s -t -g0,1,2,3,4 --buy_reason_list "buy_tag_a,buy_tag_b" --sell_reason_list "roi,custom_sell_tag_a,stop_loss" +python3 scripts/buy_reasons.py -c -s -t -g0,1,2,3,4 --buy_reason_list "buy_tag_a,buy_tag_b" --sell_reason_list "roi,custom_sell_tag_a,stop_loss" ``` ### Outputting signal candle indicators @@ -67,7 +71,7 @@ indicators. To print out a column for a given set of indicators, use the `--indi option: ```bash -freqtrade tag-analysis -c -s -t -g0,1,2,3,4 --buy_reason_list "buy_tag_a,buy_tag_b" --sell_reason_list "roi,custom_sell_tag_a,stop_loss" --indicator_list "rsi,rsi_1h,bb_lowerband,ema_9,macd,macdsignal" +python3 scripts/buy_reasons.py -c -s -t -g0,1,2,3,4 --buy_reason_list "buy_tag_a,buy_tag_b" --sell_reason_list "roi,custom_sell_tag_a,stop_loss" --indicator_list "rsi,rsi_1h,bb_lowerband,ema_9,macd,macdsignal" ``` The indicators have to be present in your strategy's main DataFrame (either for your main From 933054a51ccb991a646452e2e26f2ea7cad37229 Mon Sep 17 00:00:00 2001 From: froggleston Date: Wed, 20 Apr 2022 13:54:50 +0100 Subject: [PATCH 058/118] Move enable option text to make better sense --- docs/advanced-backtesting.md | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/docs/advanced-backtesting.md b/docs/advanced-backtesting.md index d8c6c505e..4d91c4305 100644 --- a/docs/advanced-backtesting.md +++ b/docs/advanced-backtesting.md @@ -10,7 +10,15 @@ determine indicator values on the signal candle that resulted in a trade opening !!! Note The following buy reason analysis is only available for backtesting, *not hyperopt*. -We first need to enable the exporting of trades from backtesting: +We first need to tell freqtrade to export the signal candles for each opened trade, +so add the following option to your config file: + +``` +'backtest_signal_candle_export_enable': true, +``` + +We then need to run backtesting and include the `--export` option to enable the exporting of +trades: ```bash freqtrade backtesting -c --timeframe --strategy --timerange= --export=trades @@ -20,13 +28,6 @@ To analyse the buy tags, we need to use the `buy_reasons.py` script from [froggleston's repo](https://github.com/froggleston/freqtrade-buyreasons). Follow the instructions in their README to copy the script into your `freqtrade/scripts/` folder. -We then need the signal candles for each opened trade so add the following option to your -config file: - -``` -'backtest_signal_candle_export_enable': true, -``` - This will tell freqtrade to output a pickled dictionary of strategy, pairs and corresponding DataFrame of the candles that resulted in buy signals. Depending on how many buys your strategy makes, this file may get quite large, so periodically check your `user_data/backtest_results` From f92997d3789c6eb8b9116e4a55dac2b9b7112aa7 Mon Sep 17 00:00:00 2001 From: froggleston Date: Wed, 20 Apr 2022 14:05:33 +0100 Subject: [PATCH 059/118] Move signal candle generation into separate function --- freqtrade/optimize/backtesting.py | 37 +++++++++++++++++-------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 03a6cade0..9d7f19f7a 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -1079,26 +1079,29 @@ class Backtesting: if self.backtest_signal_candle_export_enable and \ self.dataprovider.runmode == RunMode.BACKTEST: - signal_candles_only = {} - for pair in preprocessed_tmp.keys(): - signal_candles_only_df = DataFrame() - - pairdf = preprocessed_tmp[pair] - resdf = results['results'] - pairresults = resdf.loc[(resdf["pair"] == pair)] - - if pairdf.shape[0] > 0: - for t, v in pairresults.open_date.items(): - allinds = pairdf.loc[(pairdf['date'] < v)] - signal_inds = allinds.iloc[[-1]] - signal_candles_only_df = signal_candles_only_df.append(signal_inds) - - signal_candles_only[pair] = signal_candles_only_df - - self.processed_dfs[self.strategy.get_strategy_name()] = signal_candles_only + self._generate_trade_signal_candles(preprocessed_tmp, results) return min_date, max_date + def _generate_trade_signal_candles(self, preprocessed_df, bt_results): + signal_candles_only = {} + for pair in preprocessed_df.keys(): + signal_candles_only_df = DataFrame() + + pairdf = preprocessed_df[pair] + resdf = bt_results['results'] + pairresults = resdf.loc[(resdf["pair"] == pair)] + + if pairdf.shape[0] > 0: + for t, v in pairresults.open_date.items(): + allinds = pairdf.loc[(pairdf['date'] < v)] + signal_inds = allinds.iloc[[-1]] + signal_candles_only_df = signal_candles_only_df.append(signal_inds) + + signal_candles_only[pair] = signal_candles_only_df + + self.processed_dfs[self.strategy.get_strategy_name()] = signal_candles_only + def _get_min_cached_backtest_date(self): min_backtest_date = None backtest_cache_age = self.config.get('backtest_cache', constants.BACKTEST_CACHE_DEFAULT) From ba305e93edb49defc42f9a8b01b957e33984722f Mon Sep 17 00:00:00 2001 From: Patel Kaushal <36811899+koradiyakaushal@users.noreply.github.com> Date: Thu, 21 Apr 2022 18:35:41 +0530 Subject: [PATCH 060/118] Ref: timeseries friendly merge_ordered in merge_informative_pair function --- freqtrade/strategy/strategy_helper.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/freqtrade/strategy/strategy_helper.py b/freqtrade/strategy/strategy_helper.py index a36cb3dbb..fef5fb812 100644 --- a/freqtrade/strategy/strategy_helper.py +++ b/freqtrade/strategy/strategy_helper.py @@ -56,12 +56,18 @@ def merge_informative_pair(dataframe: pd.DataFrame, informative: pd.DataFrame, # Combine the 2 dataframes # all indicators on the informative sample MUST be calculated before this point - dataframe = pd.merge(dataframe, informative, left_on='date', - right_on=date_merge, how='left') + if ffill: + # https://pandas.pydata.org/docs/user_guide/merging.html#timeseries-friendly-merging + # merge_ordered - ffill method is 2.5x faster than seperate ffill() + dataframe = pd.merge_ordered(dataframe, informative, fill_method="ffill", left_on='date', + right_on=date_merge, how='left') + else: + dataframe = pd.merge(dataframe, informative, left_on='date', + right_on=date_merge, how='left') dataframe = dataframe.drop(date_merge, axis=1) - if ffill: - dataframe = dataframe.ffill() + # if ffill: + # dataframe = dataframe.ffill() return dataframe From 7f60364f63057bba06dfc3bf3ed3886560906a3b Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 22 Apr 2022 06:38:51 +0200 Subject: [PATCH 061/118] Add doc-page to index --- docs/advanced-backtesting.md | 12 ++++++------ freqtrade/optimize/backtesting.py | 8 ++++---- mkdocs.yml | 1 + 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/docs/advanced-backtesting.md b/docs/advanced-backtesting.md index 4d91c4305..69dc428f1 100644 --- a/docs/advanced-backtesting.md +++ b/docs/advanced-backtesting.md @@ -1,6 +1,6 @@ # Advanced Backtesting Analysis -## Analyse the buy/entry and sell/exit tags +## Analyze the buy/entry and sell/exit tags It can be helpful to understand how a strategy behaves according to the buy/entry tags used to mark up different buy conditions. You might want to see more complex statistics about each buy and @@ -20,11 +20,11 @@ so add the following option to your config file: We then need to run backtesting and include the `--export` option to enable the exporting of trades: -```bash +``` bash freqtrade backtesting -c --timeframe --strategy --timerange= --export=trades ``` -To analyse the buy tags, we need to use the `buy_reasons.py` script from +To analyze the buy tags, we need to use the `buy_reasons.py` script from [froggleston's repo](https://github.com/froggleston/freqtrade-buyreasons). Follow the instructions in their README to copy the script into your `freqtrade/scripts/` folder. @@ -39,9 +39,9 @@ backtesting with the `--cache none` option to make sure no cached results are us If all goes well, you should now see a `backtest-result-{timestamp}_signals.pkl` file in the `user_data/backtest_results` folder. -Now run the buy_reasons.py script, supplying a few options: +Now run the `buy_reasons.py` script, supplying a few options: -```bash +``` bash python3 scripts/buy_reasons.py -c -s -t -g0,1,2,3,4 ``` @@ -76,5 +76,5 @@ python3 scripts/buy_reasons.py -c -s -t Date: Fri, 22 Apr 2022 18:46:12 +0100 Subject: [PATCH 062/118] Add signals enum to 'export' cli option --- freqtrade/commands/arguments.py | 2 +- freqtrade/constants.py | 3 +-- freqtrade/optimize/backtesting.py | 5 +++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index 7d4624bd1..8a108fe79 100644 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -23,7 +23,7 @@ ARGS_COMMON_OPTIMIZE = ["timeframe", "timerange", "dataformat_ohlcv", ARGS_BACKTEST = ARGS_COMMON_OPTIMIZE + ["position_stacking", "use_max_market_positions", "enable_protections", "dry_run_wallet", "timeframe_detail", - "strategy_list", "export", "exportfilename", + "strategy_list", "export", "exportfilename" "backtest_breakdown", "backtest_cache"] ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + ["hyperopt", "hyperopt_path", diff --git a/freqtrade/constants.py b/freqtrade/constants.py index d21020a3f..1a21ec77f 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -14,7 +14,7 @@ PROCESS_THROTTLE_SECS = 5 # sec HYPEROPT_EPOCH = 100 # epochs RETRY_TIMEOUT = 30 # sec TIMEOUT_UNITS = ['minutes', 'seconds'] -EXPORT_OPTIONS = ['none', 'trades'] +EXPORT_OPTIONS = ['none', 'trades', 'signals'] DEFAULT_DB_PROD_URL = 'sqlite:///tradesv3.sqlite' DEFAULT_DB_DRYRUN_URL = 'sqlite:///tradesv3.dryrun.sqlite' UNLIMITED_STAKE_AMOUNT = 'unlimited' @@ -380,7 +380,6 @@ CONF_SCHEMA = { }, 'position_adjustment_enable': {'type': 'boolean'}, 'max_entry_position_adjustment': {'type': ['integer', 'number'], 'minimum': -1}, - 'backtest_signal_candle_export_enable': {'type': 'boolean'}, }, 'definitions': { 'exchange': { diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 9d7f19f7a..c2d9f1edb 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -1077,7 +1077,7 @@ class Backtesting: }) self.all_results[self.strategy.get_strategy_name()] = results - if self.backtest_signal_candle_export_enable and \ + if self.config.get('export', 'none') == 'signals' and \ self.dataprovider.runmode == RunMode.BACKTEST: self._generate_trade_signal_candles(preprocessed_tmp, results) @@ -1163,8 +1163,9 @@ class Backtesting: if self.config.get('export', 'none') == 'trades': store_backtest_stats(self.config['exportfilename'], self.results) - if self.backtest_signal_candle_export_enable and \ + if self.config.get('export', 'none') == 'signals' and \ self.dataprovider.runmode == RunMode.BACKTEST: + store_backtest_stats(self.config['exportfilename'], self.results) store_backtest_signal_candles(self.config['exportfilename'], self.processed_dfs) # Results may be mixed up now. Sort them so they follow --strategy-list order. From 2fc4e5e1172917368719fc82ec16e537298d55dc Mon Sep 17 00:00:00 2001 From: froggleston Date: Fri, 22 Apr 2022 18:54:02 +0100 Subject: [PATCH 063/118] Fix weird removal of comma --- freqtrade/commands/arguments.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index 8a108fe79..7d4624bd1 100644 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -23,7 +23,7 @@ ARGS_COMMON_OPTIMIZE = ["timeframe", "timerange", "dataformat_ohlcv", ARGS_BACKTEST = ARGS_COMMON_OPTIMIZE + ["position_stacking", "use_max_market_positions", "enable_protections", "dry_run_wallet", "timeframe_detail", - "strategy_list", "export", "exportfilename" + "strategy_list", "export", "exportfilename", "backtest_breakdown", "backtest_cache"] ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + ["hyperopt", "hyperopt_path", From dff9d52b3067a843e48c57e56e70a3942611f88d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 23 Apr 2022 08:51:52 +0200 Subject: [PATCH 064/118] Remove hints on no longer used option, add very primitive test --- docs/backtesting.md | 24 ++++++++++++------------ docs/configuration.md | 1 - freqtrade/optimize/backtesting.py | 11 ++++------- tests/optimize/test_backtesting.py | 5 ++++- 4 files changed, 20 insertions(+), 21 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index 5d836d01b..f732068f1 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -20,7 +20,8 @@ usage: freqtrade backtesting [-h] [-v] [--logfile FILE] [-V] [-c PATH] [--dry-run-wallet DRY_RUN_WALLET] [--timeframe-detail TIMEFRAME_DETAIL] [--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]] - [--export {none,trades}] [--export-filename PATH] + [--export {none,trades,signals}] + [--export-filename PATH] [--breakdown {day,week,month} [{day,week,month} ...]] [--cache {none,day,week,month}] @@ -63,18 +64,17 @@ optional arguments: `30m`, `1h`, `1d`). --strategy-list STRATEGY_LIST [STRATEGY_LIST ...] Provide a space-separated list of strategies to - backtest. Please note that timeframe needs to be - set either in config or via command line. When using - this together with `--export trades`, the strategy- - name is injected into the filename (so `backtest- - data.json` becomes `backtest-data-SampleStrategy.json` - --export {none,trades} + backtest. Please note that timeframe needs to be set + either in config or via command line. When using this + together with `--export trades`, the strategy-name is + injected into the filename (so `backtest-data.json` + becomes `backtest-data-SampleStrategy.json` + --export {none,trades,signals} Export backtest results (default: trades). - --export-filename PATH - Save backtest results to the file with this filename. - Requires `--export` to be set as well. Example: - `--export-filename=user_data/backtest_results/backtest - _today.json` + --export-filename PATH, --backtest-filename PATH + Use this filename for backtest results.Requires + `--export` to be set as well. Example: `--export-filen + ame=user_data/backtest_results/backtest_today.json` --breakdown {day,week,month} [{day,week,month} ...] Show backtesting breakdown per [day, week, month]. --cache {none,day,week,month} diff --git a/docs/configuration.md b/docs/configuration.md index 061b6c77c..5770450a6 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -229,7 +229,6 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `dataformat_trades` | Data format to use to store historical trades data.
*Defaults to `jsongz`*.
**Datatype:** String | `position_adjustment_enable` | Enables the strategy to use position adjustments (additional buys or sells). [More information here](strategy-callbacks.md#adjust-trade-position).
[Strategy Override](#parameters-in-the-strategy).
*Defaults to `false`.*
**Datatype:** Boolean | `max_entry_position_adjustment` | Maximum additional order(s) for each open trade on top of the first entry Order. Set it to `-1` for unlimited additional orders. [More information here](strategy-callbacks.md#adjust-trade-position).
[Strategy Override](#parameters-in-the-strategy).
*Defaults to `-1`.*
**Datatype:** Positive Integer or -1 -| `backtest_signal_candle_export_enable` | Enables the exporting of signal candles for use in post-backtesting analysis of buy tags. See [Strategy Analysis](strategy_analysis_example.md#analyse-the-buy-entry-and-sell-exit-tags).
*Defaults to `false`.*
**Datatype:** Boolean ### Parameters in the strategy diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index c2d9f1edb..f4149fdc1 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -131,9 +131,6 @@ class Backtesting: self.config['startup_candle_count'] = self.required_startup self.exchange.validate_required_startup_candles(self.required_startup, self.timeframe) - self.backtest_signal_candle_export_enable = self.config.get( - 'backtest_signal_candle_export_enable', False) - self.trading_mode: TradingMode = config.get('trading_mode', TradingMode.SPOT) # strategies which define "can_short=True" will fail to load in Spot mode. self._can_short = self.trading_mode != TradingMode.SPOT @@ -1077,8 +1074,8 @@ class Backtesting: }) self.all_results[self.strategy.get_strategy_name()] = results - if self.config.get('export', 'none') == 'signals' and \ - self.dataprovider.runmode == RunMode.BACKTEST: + if (self.config.get('export', 'none') == 'signals' and + self.dataprovider.runmode == RunMode.BACKTEST): self._generate_trade_signal_candles(preprocessed_tmp, results) return min_date, max_date @@ -1163,8 +1160,8 @@ class Backtesting: if self.config.get('export', 'none') == 'trades': store_backtest_stats(self.config['exportfilename'], self.results) - if self.config.get('export', 'none') == 'signals' and \ - self.dataprovider.runmode == RunMode.BACKTEST: + if (self.config.get('export', 'none') == 'signals' and + self.dataprovider.runmode == RunMode.BACKTEST): store_backtest_stats(self.config['exportfilename'], self.results) store_backtest_signal_candles(self.config['exportfilename'], self.processed_dfs) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 08957acf9..797d3bafa 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -384,14 +384,16 @@ def test_backtesting_start(default_conf, mocker, testdatadir, caplog) -> None: mocker.patch('freqtrade.optimize.backtesting.generate_backtest_stats') mocker.patch('freqtrade.optimize.backtesting.show_backtest_results') sbs = mocker.patch('freqtrade.optimize.backtesting.store_backtest_stats') + sbc = mocker.patch('freqtrade.optimize.backtesting.store_backtest_signal_candles') mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist', PropertyMock(return_value=['UNITTEST/BTC'])) default_conf['timeframe'] = '1m' default_conf['datadir'] = testdatadir - default_conf['export'] = 'trades' + default_conf['export'] = 'signals' default_conf['exportfilename'] = 'export.txt' default_conf['timerange'] = '-1510694220' + default_conf['runmode'] = RunMode.BACKTEST backtesting = Backtesting(default_conf) backtesting._set_strategy(backtesting.strategylist[0]) @@ -407,6 +409,7 @@ def test_backtesting_start(default_conf, mocker, testdatadir, caplog) -> None: assert backtesting.strategy.dp._pairlists is not None assert backtesting.strategy.bot_loop_start.call_count == 1 assert sbs.call_count == 1 + assert sbc.call_count == 1 def test_backtesting_start_no_data(default_conf, mocker, caplog, testdatadir) -> None: From c6927a1501d76b4e8b61ced9f5d1c53dbee75006 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 23 Apr 2022 09:10:15 +0200 Subject: [PATCH 065/118] Fix argument spelling --- freqtrade/commands/arguments.py | 3 ++- freqtrade/commands/cli_options.py | 4 ++-- freqtrade/resolvers/iresolver.py | 9 ++++----- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index 2fb8d3258..e66f100c0 100644 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -37,7 +37,8 @@ ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + ["hyperopt", "hyperopt_path", ARGS_EDGE = ARGS_COMMON_OPTIMIZE + ["stoploss_range"] -ARGS_LIST_STRATEGIES = ["strategy_path", "print_one_column", "print_colorized"] +ARGS_LIST_STRATEGIES = ["strategy_path", "print_one_column", "print_colorized", + "recursive_strategy_search"] ARGS_LIST_HYPEROPTS = ["hyperopt_path", "print_one_column", "print_colorized"] diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index 095aad6c3..548a7f473 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -83,9 +83,9 @@ AVAILABLE_CLI_OPTIONS = { action='store_true', ), "recursive_strategy_search": Arg( - '--recursive_strategy_search', + '--recursive-strategy-search', help='Recursively search for a strategy in the strategies folder.', - metavar='store_true', + action='store_true', ), # Main options "strategy": Arg( diff --git a/freqtrade/resolvers/iresolver.py b/freqtrade/resolvers/iresolver.py index cddc8b84d..d310856d8 100644 --- a/freqtrade/resolvers/iresolver.py +++ b/freqtrade/resolvers/iresolver.py @@ -44,7 +44,7 @@ class IResolver: @classmethod def build_search_paths(cls, config: Dict[str, Any], user_subdir: Optional[str] = None, - extra_dirs: Optional[List[str]] = None) -> List[Path]: + extra_dirs: List[str] = []) -> List[Path]: abs_paths: List[Path] = [] if cls.initial_search_path: @@ -53,10 +53,9 @@ class IResolver: if user_subdir: abs_paths.insert(0, config['user_data_dir'].joinpath(user_subdir)) - if extra_dirs: - # Add extra directory to the top of the search paths - for dir in extra_dirs: - abs_paths.insert(0, Path(dir).resolve()) + # Add extra directory to the top of the search paths + for dir in extra_dirs: + abs_paths.insert(0, Path(dir).resolve()) return abs_paths From ba92e09b7bab024fb7ea7a12d1b3df22cf33fc6a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 23 Apr 2022 09:11:50 +0200 Subject: [PATCH 066/118] list-strategies should find recursively as well --- config_examples/config_full.example.json | 1 + freqtrade/commands/list_commands.py | 9 +++++---- freqtrade/optimize/hyperopt_tools.py | 3 ++- freqtrade/resolvers/iresolver.py | 11 +++++++++-- freqtrade/resolvers/strategy_resolver.py | 2 +- freqtrade/rpc/api_server/api_v1.py | 3 ++- 6 files changed, 20 insertions(+), 9 deletions(-) diff --git a/config_examples/config_full.example.json b/config_examples/config_full.example.json index 7931476b4..8f14e1771 100644 --- a/config_examples/config_full.example.json +++ b/config_examples/config_full.example.json @@ -179,6 +179,7 @@ "disable_dataframe_checks": false, "strategy": "SampleStrategy", "strategy_path": "user_data/strategies/", + "recursive_strategy_search": false, "dataformat_ohlcv": "json", "dataformat_trades": "jsongz" } diff --git a/freqtrade/commands/list_commands.py b/freqtrade/commands/list_commands.py index 38fb098a0..1833db922 100644 --- a/freqtrade/commands/list_commands.py +++ b/freqtrade/commands/list_commands.py @@ -41,7 +41,7 @@ def start_list_exchanges(args: Dict[str, Any]) -> None: print(tabulate(exchanges, headers=['Exchange name', 'Valid', 'reason'])) -def _print_objs_tabular(objs: List, print_colorized: bool) -> None: +def _print_objs_tabular(objs: List, print_colorized: bool, base_dir: Path) -> None: if print_colorized: colorama_init(autoreset=True) red = Fore.RED @@ -55,7 +55,7 @@ def _print_objs_tabular(objs: List, print_colorized: bool) -> None: names = [s['name'] for s in objs] objs_to_print = [{ 'name': s['name'] if s['name'] else "--", - 'location': s['location'].name, + 'location': s['location'].relative_to(base_dir), 'status': (red + "LOAD FAILED" + reset if s['class'] is None else "OK" if names.count(s['name']) == 1 else yellow + "DUPLICATE NAME" + reset) @@ -77,7 +77,8 @@ def start_list_strategies(args: Dict[str, Any]) -> None: config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) directory = Path(config.get('strategy_path', config['user_data_dir'] / USERPATH_STRATEGIES)) - strategy_objs = StrategyResolver.search_all_objects(directory, not args['print_one_column']) + strategy_objs = StrategyResolver.search_all_objects( + directory, not args['print_one_column'], config.get('recursive_strategy_search', False)) # Sort alphabetically strategy_objs = sorted(strategy_objs, key=lambda x: x['name']) for obj in strategy_objs: @@ -89,7 +90,7 @@ def start_list_strategies(args: Dict[str, Any]) -> None: if args['print_one_column']: print('\n'.join([s['name'] for s in strategy_objs])) else: - _print_objs_tabular(strategy_objs, config.get('print_colorized', False)) + _print_objs_tabular(strategy_objs, config.get('print_colorized', False), directory) def start_list_timeframes(args: Dict[str, Any]) -> None: diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py index 8c84f772a..e836681c5 100755 --- a/freqtrade/optimize/hyperopt_tools.py +++ b/freqtrade/optimize/hyperopt_tools.py @@ -41,7 +41,8 @@ class HyperoptTools(): """ from freqtrade.resolvers.strategy_resolver import StrategyResolver directory = Path(config.get('strategy_path', config['user_data_dir'] / USERPATH_STRATEGIES)) - strategy_objs = StrategyResolver.search_all_objects(directory, False) + strategy_objs = StrategyResolver.search_all_objects( + directory, False, config.get('recursive_strategy_search', False)) strategies = [s for s in strategy_objs if s['name'] == strategy_name] if strategies: strategy = strategies[0] diff --git a/freqtrade/resolvers/iresolver.py b/freqtrade/resolvers/iresolver.py index d310856d8..74b28dffe 100644 --- a/freqtrade/resolvers/iresolver.py +++ b/freqtrade/resolvers/iresolver.py @@ -182,18 +182,25 @@ class IResolver: ) @classmethod - def search_all_objects(cls, directory: Path, - enum_failed: bool) -> List[Dict[str, Any]]: + def search_all_objects(cls, directory: Path, enum_failed: bool, + recursive: bool = False) -> List[Dict[str, Any]]: """ Searches a directory for valid objects :param directory: Path to search :param enum_failed: If True, will return None for modules which fail. Otherwise, failing modules are skipped. + :param recursive: Recursively walk directory tree searching for strategies :return: List of dicts containing 'name', 'class' and 'location' entries """ logger.debug(f"Searching for {cls.object_type.__name__} '{directory}'") objects = [] for entry in directory.iterdir(): + if ( + recursive and entry.is_dir() + and not entry.name.startswith('__') + and not entry.name.startswith('.') + ): + objects.extend(cls.search_all_objects(entry, enum_failed, recursive=recursive)) # Only consider python files if entry.suffix != '.py': logger.debug('Ignoring %s', entry) diff --git a/freqtrade/resolvers/strategy_resolver.py b/freqtrade/resolvers/strategy_resolver.py index 8a22dbd65..60961b15b 100644 --- a/freqtrade/resolvers/strategy_resolver.py +++ b/freqtrade/resolvers/strategy_resolver.py @@ -167,7 +167,7 @@ class StrategyResolver(IResolver): :param extra_dir: additional directory to search for the given strategy :return: Strategy instance or None """ - if 'recursive_strategy_search' in config and config['recursive_strategy_search']: + if config.get('recursive_strategy_search', False): extra_dirs: List[str] = [ path[0] for path in walk(f"{config['user_data_dir']}/{USERPATH_STRATEGIES}") ] # sub-directories diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index 5a34385da..fe6426178 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -243,7 +243,8 @@ def list_strategies(config=Depends(get_config)): directory = Path(config.get( 'strategy_path', config['user_data_dir'] / USERPATH_STRATEGIES)) from freqtrade.resolvers.strategy_resolver import StrategyResolver - strategies = StrategyResolver.search_all_objects(directory, False) + strategies = StrategyResolver.search_all_objects( + directory, False, config.get('recursive_strategy_search', False)) strategies = sorted(strategies, key=lambda x: x['name']) return {'strategies': [x['name'] for x in strategies]} From aa5345190ec36bb20d03cc66c16c958c371b9214 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 23 Apr 2022 09:19:18 +0200 Subject: [PATCH 067/118] Test recursive strategy-listing --- tests/commands/test_commands.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index 1431bd22a..39294e568 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -892,6 +892,26 @@ def test_start_list_strategies(mocker, caplog, capsys): assert "legacy_strategy_v1.py" in captured.out assert CURRENT_TEST_STRATEGY in captured.out assert "LOAD FAILED" in captured.out + # Recursive + assert "TestStrategyNoImplements" not in captured.out + + # Test recursive + args = [ + "list-strategies", + "--strategy-path", + str(Path(__file__).parent.parent / "strategy" / "strats"), + '--no-color', + '--recursive-strategy-search' + ] + pargs = get_args(args) + # pargs['config'] = None + start_list_strategies(pargs) + captured = capsys.readouterr() + assert "TestStrategyLegacyV1" in captured.out + assert "legacy_strategy_v1.py" in captured.out + assert "StrategyTestV2" in captured.out + assert "TestStrategyNoImplements" in captured.out + assert "broken_strats/broken_futures_strategies.py" in captured.out def test_start_test_pairlist(mocker, caplog, tickers, default_conf, capsys): From 580a2c6545ce79ffda7b54f2e1a928426d519aef Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 23 Apr 2022 09:23:53 +0200 Subject: [PATCH 068/118] Don't repeat backtest-storing --- freqtrade/optimize/backtesting.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index f4149fdc1..f5571c4e2 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -1157,12 +1157,11 @@ class Backtesting: else: self.results = results - if self.config.get('export', 'none') == 'trades': + if self.config.get('export', 'none') in ('trades', 'signals'): store_backtest_stats(self.config['exportfilename'], self.results) if (self.config.get('export', 'none') == 'signals' and self.dataprovider.runmode == RunMode.BACKTEST): - store_backtest_stats(self.config['exportfilename'], self.results) store_backtest_signal_candles(self.config['exportfilename'], self.processed_dfs) # Results may be mixed up now. Sort them so they follow --strategy-list order. From 5a90d5ece67dc76866bb675fbe7a7f3f6ddbbaa2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 23 Apr 2022 09:44:04 +0200 Subject: [PATCH 069/118] Fix docstring quotes --- freqtrade/optimize/hyperopt.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index babcc5491..78d237c8d 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -410,7 +410,7 @@ class Hyperopt: dump(preprocessed, self.data_pickle_file) def get_asked_points(self, n_points: int) -> Tuple[List[List[Any]], List[bool]]: - ''' + """ Enforce points returned from `self.opt.ask` have not been already evaluated Steps: @@ -420,7 +420,7 @@ class Hyperopt: 4. If still some points are missing in respect to `n_points`, random sample some points 5. Repeat until at least `n_points` points in the `asked_non_tried` list 6. Return a list with length truncated at `n_points` - ''' + """ def unique_list(a_list): new_list = [] for item in a_list: From 30f314d580807bdb83969981293c162bdafc9a76 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 23 Apr 2022 10:44:11 +0200 Subject: [PATCH 070/118] windows compatibility of test --- tests/commands/test_commands.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index 39294e568..d1f54ad52 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -847,7 +847,7 @@ def test_start_convert_trades(mocker, caplog): assert convert_mock.call_count == 1 -def test_start_list_strategies(mocker, caplog, capsys): +def test_start_list_strategies(capsys): args = [ "list-strategies", @@ -911,7 +911,7 @@ def test_start_list_strategies(mocker, caplog, capsys): assert "legacy_strategy_v1.py" in captured.out assert "StrategyTestV2" in captured.out assert "TestStrategyNoImplements" in captured.out - assert "broken_strats/broken_futures_strategies.py" in captured.out + assert str(Path("broken_strats/broken_futures_strategies.py")) in captured.out def test_start_test_pairlist(mocker, caplog, tickers, default_conf, capsys): From 84f5a4d5bc0e116ba1b7a5a476c9d4e717561cf3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 23 Apr 2022 10:51:24 +0200 Subject: [PATCH 071/118] Fix indentation --- freqtrade/strategy/strategy_helper.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/strategy/strategy_helper.py b/freqtrade/strategy/strategy_helper.py index fef5fb812..43728dc1f 100644 --- a/freqtrade/strategy/strategy_helper.py +++ b/freqtrade/strategy/strategy_helper.py @@ -60,10 +60,10 @@ def merge_informative_pair(dataframe: pd.DataFrame, informative: pd.DataFrame, # https://pandas.pydata.org/docs/user_guide/merging.html#timeseries-friendly-merging # merge_ordered - ffill method is 2.5x faster than seperate ffill() dataframe = pd.merge_ordered(dataframe, informative, fill_method="ffill", left_on='date', - right_on=date_merge, how='left') + right_on=date_merge, how='left') else: dataframe = pd.merge(dataframe, informative, left_on='date', - right_on=date_merge, how='left') + right_on=date_merge, how='left') dataframe = dataframe.drop(date_merge, axis=1) # if ffill: From 1120392f39f5e0f9fde546592e2a7e0b330f9e25 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 23 Apr 2022 11:12:43 +0200 Subject: [PATCH 072/118] Fix pre-commit indentation --- .github/ISSUE_TEMPLATE/feature_request.md | 1 - .github/workflows/docker_update_readme.yml | 1 - .pre-commit-config.yaml | 34 +++++++++++++++++----- .pylintrc | 1 - docs/javascripts/config.js | 2 +- freqtrade.service | 1 - freqtrade.service.watchdog | 1 - requirements-plot.txt | 1 - 8 files changed, 27 insertions(+), 15 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index c32fb33c2..a18915462 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -24,4 +24,3 @@ Have you search for this feature before requesting it? It's highly likely that a ## Describe the enhancement *Explain the enhancement you would like* - diff --git a/.github/workflows/docker_update_readme.yml b/.github/workflows/docker_update_readme.yml index 822533ee2..4587626f6 100644 --- a/.github/workflows/docker_update_readme.yml +++ b/.github/workflows/docker_update_readme.yml @@ -15,4 +15,3 @@ jobs: DOCKERHUB_USERNAME: ${{ secrets.DOCKER_USERNAME }} DOCKERHUB_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} DOCKERHUB_REPOSITORY: freqtradeorg/freqtrade - diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 31af5b7c7..316baf0e3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,21 +1,39 @@ # See https://pre-commit.com for more information # See https://pre-commit.com/hooks.html for more hooks repos: -- repo: https://github.com/pycqa/flake8 - rev: '4.0.1' + - repo: https://github.com/pycqa/flake8 + rev: "4.0.1" hooks: - - id: flake8 + - id: flake8 # stages: [push] -- repo: https://github.com/pre-commit/mirrors-mypy - rev: 'v0.942' + - repo: https://github.com/pre-commit/mirrors-mypy + rev: "v0.942" hooks: - - id: mypy + - id: mypy # stages: [push] -- repo: https://github.com/pycqa/isort - rev: '5.10.1' + - repo: https://github.com/pycqa/isort + rev: "5.10.1" hooks: - id: isort name: isort (python) # stages: [push] + + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v2.4.0 + hooks: + - id: end-of-file-fixer + exclude: | + (?x)^( + tests/.*| + .*\.svg + )$ + - id: mixed-line-ending + - id: debug-statements + - id: check-ast + - id: trailing-whitespace + exclude: | + (?x)^( + .*\.md + )$ diff --git a/.pylintrc b/.pylintrc index dce99c067..0932ecba4 100644 --- a/.pylintrc +++ b/.pylintrc @@ -7,4 +7,3 @@ ignore=vendor [TYPECHECK] ignored-modules=numpy,talib,talib.abstract - diff --git a/docs/javascripts/config.js b/docs/javascripts/config.js index 95d619efc..80e81ba59 100644 --- a/docs/javascripts/config.js +++ b/docs/javascripts/config.js @@ -9,4 +9,4 @@ window.MathJax = { ignoreHtmlClass: ".*|", processHtmlClass: "arithmatex" } -}; \ No newline at end of file +}; diff --git a/freqtrade.service b/freqtrade.service index df220ed39..6f0c73ee4 100644 --- a/freqtrade.service +++ b/freqtrade.service @@ -11,4 +11,3 @@ Restart=on-failure [Install] WantedBy=default.target - diff --git a/freqtrade.service.watchdog b/freqtrade.service.watchdog index 66ea00d76..dcd32ae18 100644 --- a/freqtrade.service.watchdog +++ b/freqtrade.service.watchdog @@ -27,4 +27,3 @@ WatchdogSec=20 [Install] WantedBy=default.target - diff --git a/requirements-plot.txt b/requirements-plot.txt index 9eb6a10a3..d9faed301 100644 --- a/requirements-plot.txt +++ b/requirements-plot.txt @@ -2,4 +2,3 @@ -r requirements.txt plotly==5.7.0 - From a2af7b4fd82ef5b1697aa427967a70cfff53f876 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 23 Apr 2022 11:25:20 +0200 Subject: [PATCH 073/118] Test non-ffill approach --- tests/strategy/test_strategy_helpers.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/strategy/test_strategy_helpers.py b/tests/strategy/test_strategy_helpers.py index 65fb9f6dc..244fd3919 100644 --- a/tests/strategy/test_strategy_helpers.py +++ b/tests/strategy/test_strategy_helpers.py @@ -68,6 +68,21 @@ def test_merge_informative_pair(): assert result.iloc[7]['date_1h'] == result.iloc[4]['date'] assert result.iloc[8]['date_1h'] == result.iloc[4]['date'] + informative = generate_test_data('1h', 40) + result = merge_informative_pair(data, informative, '15m', '1h', ffill=False) + # First 3 rows are empty + assert result.iloc[0]['date_1h'] is pd.NaT + assert result.iloc[1]['date_1h'] is pd.NaT + assert result.iloc[2]['date_1h'] is pd.NaT + # Next 4 rows contain the starting date (0:00) + assert result.iloc[3]['date_1h'] == result.iloc[0]['date'] + assert result.iloc[4]['date_1h'] is pd.NaT + assert result.iloc[5]['date_1h'] is pd.NaT + assert result.iloc[6]['date_1h'] is pd.NaT + # Next 4 rows contain the next Hourly date original date row 4 + assert result.iloc[7]['date_1h'] == result.iloc[4]['date'] + assert result.iloc[8]['date_1h'] is pd.NaT + def test_merge_informative_pair_same(): data = generate_test_data('15m', 40) From f2912f88150e70256d163d904652862c4c398b8e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 23 Apr 2022 11:31:12 +0200 Subject: [PATCH 074/118] Improve mypy runs --- freqtrade/optimize/hyperopt.py | 6 +++--- freqtrade/optimize/optimize_reports.py | 2 +- setup.cfg | 1 + 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 78d237c8d..3ae975ca7 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -290,7 +290,7 @@ class Hyperopt: self.assign_params(params_dict, 'protection') if HyperoptTools.has_space(self.config, 'roi'): - self.backtesting.strategy.minimal_roi = ( # type: ignore + self.backtesting.strategy.minimal_roi = ( self.custom_hyperopt.generate_roi_table(params_dict)) if HyperoptTools.has_space(self.config, 'stoploss'): @@ -465,8 +465,8 @@ class Hyperopt: # We don't need exchange instance anymore while running hyperopt self.backtesting.exchange.close() - self.backtesting.exchange._api = None # type: ignore - self.backtesting.exchange._api_async = None # type: ignore + self.backtesting.exchange._api = None + self.backtesting.exchange._api_async = None self.backtesting.exchange.loop = None # type: ignore # self.backtesting.exchange = None # type: ignore self.backtesting.pairlists = None # type: ignore diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 6288ee16a..0ceb3a411 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -264,7 +264,7 @@ def generate_edge_table(results: dict) -> str: # Ignore type as floatfmt does allow tuples but mypy does not know that return tabulate(tabular_data, headers=headers, - floatfmt=floatfmt, tablefmt="orgtbl", stralign="right") # type: ignore + floatfmt=floatfmt, tablefmt="orgtbl", stralign="right") def _get_resample_from_period(period: str) -> str: diff --git a/setup.cfg b/setup.cfg index f4a90bda7..a33ceda1f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -52,6 +52,7 @@ exclude = [mypy] ignore_missing_imports = True +warn_unused_ignores = True [mypy-tests.*] ignore_errors = True From c1a7fc873d5c447c1e985e17ba1773c24582f47c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 23 Apr 2022 11:47:10 +0200 Subject: [PATCH 075/118] Speed up ci by running coverage only where necessary --- .github/workflows/ci.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b8df7ab10..4e18127fb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -157,6 +157,12 @@ jobs: pip install -e . - name: Tests + if: (runner.os != 'Linux' || matrix.python-version != '3.8') + run: | + pytest --random-order + + - name: Tests (with cov) + if: (runner.os == 'Linux' && matrix.python-version == '3.8') run: | pytest --random-order --cov=freqtrade --cov-config=.coveragerc From 3586c2e984845b95189cd2e0efc20fbe76425334 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 23 Apr 2022 14:22:06 +0200 Subject: [PATCH 076/118] Windows no random order --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4e18127fb..1902a6c45 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -235,7 +235,7 @@ jobs: - name: Tests run: | - pytest --random-order --cov=freqtrade --cov-config=.coveragerc + pytest --random-order - name: Backtesting run: | From 12d03e6a91dbc2e383d071540455382d0ba953d4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 23 Apr 2022 14:53:47 +0200 Subject: [PATCH 077/118] Remove unused test methods --- tests/conftest.py | 42 ---------------------------------------- tests/edge/test_edge.py | 43 ----------------------------------------- 2 files changed, 85 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index b1dcdbbd7..cc07de1de 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1632,40 +1632,6 @@ def limit_buy_order(limit_buy_order_open): return order -@pytest.fixture(scope='function') -def market_buy_order(): - return { - 'id': 'mocked_market_buy', - 'type': 'market', - 'side': 'buy', - 'symbol': 'mocked', - 'timestamp': arrow.utcnow().int_timestamp * 1000, - 'datetime': arrow.utcnow().isoformat(), - 'price': 0.00004099, - 'amount': 91.99181073, - 'filled': 91.99181073, - 'remaining': 0.0, - 'status': 'closed' - } - - -@pytest.fixture -def market_sell_order(): - return { - 'id': 'mocked_limit_sell', - 'type': 'market', - 'side': 'sell', - 'symbol': 'mocked', - 'timestamp': arrow.utcnow().int_timestamp * 1000, - 'datetime': arrow.utcnow().isoformat(), - 'price': 0.00004173, - 'amount': 91.99181073, - 'filled': 91.99181073, - 'remaining': 0.0, - 'status': 'closed' - } - - @pytest.fixture def limit_buy_order_old(): return { @@ -2946,14 +2912,6 @@ def limit_order(limit_buy_order_usdt, limit_sell_order_usdt): } -@pytest.fixture(scope='function') -def market_order(market_buy_order_usdt, market_sell_order_usdt): - return { - 'buy': market_buy_order_usdt, - 'sell': market_sell_order_usdt - } - - @pytest.fixture(scope='function') def limit_order_open(limit_buy_order_usdt_open, limit_sell_order_usdt_open): return { diff --git a/tests/edge/test_edge.py b/tests/edge/test_edge.py index a43e82b22..aa7eefd27 100644 --- a/tests/edge/test_edge.py +++ b/tests/edge/test_edge.py @@ -30,49 +30,6 @@ from tests.optimize import (BTContainer, BTrade, _build_backtest_dataframe, tests_start_time = arrow.get(2018, 10, 3) timeframe_in_minute = 60 -# Helpers for this test file - - -def _validate_ohlc(buy_ohlc_sell_matrice): - for index, ohlc in enumerate(buy_ohlc_sell_matrice): - # if not high < open < low or not high < close < low - if not ohlc[3] >= ohlc[2] >= ohlc[4] or not ohlc[3] >= ohlc[5] >= ohlc[4]: - raise Exception('Line ' + str(index + 1) + ' of ohlc has invalid values!') - return True - - -def _build_dataframe(buy_ohlc_sell_matrice): - _validate_ohlc(buy_ohlc_sell_matrice) - data = [] - for ohlc in buy_ohlc_sell_matrice: - d = { - 'date': tests_start_time.shift( - minutes=( - ohlc[0] * - timeframe_in_minute)).int_timestamp * - 1000, - 'buy': ohlc[1], - 'open': ohlc[2], - 'high': ohlc[3], - 'low': ohlc[4], - 'close': ohlc[5], - 'sell': ohlc[6]} - data.append(d) - - frame = DataFrame(data) - frame['date'] = to_datetime(frame['date'], - unit='ms', - utc=True, - infer_datetime_format=True) - - return frame - - -def _time_on_candle(number): - return np.datetime64(tests_start_time.shift( - minutes=(number * timeframe_in_minute)).int_timestamp * 1000, 'ms') - - # End helper functions # Open trade should be removed from the end tc0 = BTContainer(data=[ From 0807d3106fea5c4d4f54050f64b8f808f6d4501c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 23 Apr 2022 15:34:40 +0200 Subject: [PATCH 078/118] Remove unused import --- tests/edge/test_edge.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/edge/test_edge.py b/tests/edge/test_edge.py index aa7eefd27..b30d6f998 100644 --- a/tests/edge/test_edge.py +++ b/tests/edge/test_edge.py @@ -8,7 +8,7 @@ from unittest.mock import MagicMock import arrow import numpy as np import pytest -from pandas import DataFrame, to_datetime +from pandas import DataFrame from freqtrade.data.converter import ohlcv_to_dataframe from freqtrade.edge import Edge, PairInfo From 2d07cbce597ed3e3053a3d31db0f8f87d35d4c68 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 23 Apr 2022 17:05:41 +0200 Subject: [PATCH 079/118] Fix bad pre-commit installation closes #6713 --- setup.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.sh b/setup.sh index 5cde1a589..e0b010387 100755 --- a/setup.sh +++ b/setup.sh @@ -90,7 +90,7 @@ function updateenv() { echo "pip install completed" echo if [[ $dev =~ ^[Yy]$ ]]; then - ${PYTHON} -m pre-commit install + ${PYTHON} -m pre_commit install if [ $? -ne 0 ]; then echo "Failed installing pre-commit" exit 1 From 8cac0a47cca89bea4b69de00cbf54505679dcd24 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 23 Apr 2022 17:08:34 +0200 Subject: [PATCH 080/118] Fix joblib being in wrong requirements --- environment.yml | 2 +- freqtrade/misc.py | 2 +- requirements-hyperopt.txt | 1 - requirements.txt | 1 + 4 files changed, 3 insertions(+), 3 deletions(-) diff --git a/environment.yml b/environment.yml index f2f961894..19f3c7f5a 100644 --- a/environment.yml +++ b/environment.yml @@ -32,6 +32,7 @@ dependencies: - prompt-toolkit - schedule - python-dateutil + - joblib # ============================ @@ -54,7 +55,6 @@ dependencies: - scikit-learn - filelock - scikit-optimize - - joblib - progressbar2 # ============================ # 4/4 req plot diff --git a/freqtrade/misc.py b/freqtrade/misc.py index be12d8224..55a533725 100644 --- a/freqtrade/misc.py +++ b/freqtrade/misc.py @@ -12,7 +12,6 @@ from typing import Any, Iterator, List, Union from typing.io import IO from urllib.parse import urlparse -import joblib import rapidjson from freqtrade.constants import DECIMAL_PER_COIN_FALLBACK, DECIMALS_PER_COIN @@ -94,6 +93,7 @@ def file_dump_joblib(filename: Path, data: Any, log: bool = True) -> None: :param data: Object data to save :return: """ + import joblib if log: logger.info(f'dumping joblib to "{filename}"') diff --git a/requirements-hyperopt.txt b/requirements-hyperopt.txt index aeb7be035..32fc3f4b9 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -6,5 +6,4 @@ scipy==1.8.0 scikit-learn==1.0.2 scikit-optimize==0.9.0 filelock==3.6.0 -joblib==1.1.0 progressbar2==4.0.0 diff --git a/requirements.txt b/requirements.txt index 24f267b67..571d1892c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,6 +20,7 @@ pycoingecko==2.2.0 jinja2==3.1.1 tables==3.7.0 blosc==1.10.6 +joblib==1.1.0 # find first, C search in arrays py_find_1st==1.1.5 From acec5640143671600878c769d8d7dacc017c5434 Mon Sep 17 00:00:00 2001 From: froggleston Date: Sat, 23 Apr 2022 17:18:38 +0100 Subject: [PATCH 081/118] Update advanced backtesting docs to match fixed buy_reasons script --- docs/advanced-backtesting.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/advanced-backtesting.md b/docs/advanced-backtesting.md index 8a8c1af77..2a484da69 100644 --- a/docs/advanced-backtesting.md +++ b/docs/advanced-backtesting.md @@ -17,15 +17,15 @@ signals **and** trades: freqtrade backtesting -c --timeframe --strategy --timerange= --export=signals ``` -To analyze the buy tags, we need to use the `buy_reasons.py` script from -[froggleston's repo](https://github.com/froggleston/freqtrade-buyreasons). Follow the instructions -in their README to copy the script into your `freqtrade/scripts/` folder. - This will tell freqtrade to output a pickled dictionary of strategy, pairs and corresponding DataFrame of the candles that resulted in buy signals. Depending on how many buys your strategy makes, this file may get quite large, so periodically check your `user_data/backtest_results` folder to delete old exports. +To analyze the buy tags, we need to use the `buy_reasons.py` script from +[froggleston's repo](https://github.com/froggleston/freqtrade-buyreasons). Follow the instructions +in their README to copy the script into your `freqtrade/scripts/` folder. + Before running your next backtest, make sure you either delete your old backtest results or run backtesting with the `--cache none` option to make sure no cached results are used. @@ -47,14 +47,14 @@ running with the `-h` option. To show only certain buy and sell tags in the displayed output, use the following two options: ``` ---buy_reason_list : Comma separated list of buy signals to analyse. Default: "all" ---sell_reason_list : Comma separated list of sell signals to analyse. Default: "stop_loss,trailing_stop_loss" +--enter_reason_list : Comma separated list of enter signals to analyse. Default: "all" +--exit_reason_list : Comma separated list of exit signals to analyse. Default: "stop_loss,trailing_stop_loss" ``` For example: ```bash -python3 scripts/buy_reasons.py -c -s -t -g0,1,2,3,4 --buy_reason_list "buy_tag_a,buy_tag_b" --sell_reason_list "roi,custom_sell_tag_a,stop_loss" +python3 scripts/buy_reasons.py -c -s -t -g0,1,2,3,4 --enter_reason_list "enter_tag_a,enter_tag_b" --exit_reason_list "roi,custom_exit_tag_a,stop_loss" ``` ### Outputting signal candle indicators @@ -65,7 +65,7 @@ indicators. To print out a column for a given set of indicators, use the `--indi option: ```bash -python3 scripts/buy_reasons.py -c -s -t -g0,1,2,3,4 --buy_reason_list "buy_tag_a,buy_tag_b" --sell_reason_list "roi,custom_sell_tag_a,stop_loss" --indicator_list "rsi,rsi_1h,bb_lowerband,ema_9,macd,macdsignal" +python3 scripts/buy_reasons.py -c -s -t -g0,1,2,3,4 --enter_reason_list "enter_tag_a,enter_tag_b" --exit_reason_list "roi,custom_exit_tag_a,stop_loss" --indicator_list "rsi,rsi_1h,bb_lowerband,ema_9,macd,macdsignal" ``` The indicators have to be present in your strategy's main DataFrame (either for your main From 3c17409bd71ab19823db3416a783d894b70886a3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 24 Apr 2022 14:28:15 +0200 Subject: [PATCH 082/118] Update buy to entry in backtesting --- freqtrade/optimize/backtesting.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index f5571c4e2..16f94e083 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -332,11 +332,11 @@ class Backtesting: self.dataprovider._set_cached_df( pair, self.timeframe, df_analyzed, self.config['candle_type_def']) - # Create a copy of the dataframe before shifting, that way the buy signal/tag + # Create a copy of the dataframe before shifting, that way the entry signal/tag # remains on the correct candle for callbacks. df_analyzed = df_analyzed.copy() - # To avoid using data from future, we use buy/sell signals shifted + # To avoid using data from future, we use entry/exit signals shifted # from the previous candle for col in headers[5:]: tag_col = col in ('enter_tag', 'exit_tag') @@ -649,7 +649,7 @@ class Backtesting: proposed_rate=propose_rate, entry_tag=entry_tag, side=direction, ) # default value is the open rate - # We can't place orders higher than current high (otherwise it'd be a stop limit buy) + # We can't place orders higher than current high (otherwise it'd be a stop limit entry) # which freqtrade does not support in live. if direction == "short": propose_rate = max(propose_rate, row[LOW_IDX]) @@ -813,7 +813,7 @@ class Backtesting: if len(open_trades[pair]) > 0: for trade in open_trades[pair]: if trade.open_order_id and trade.nr_of_successful_entries == 0: - # Ignore trade if buy-order did not fill yet + # Ignore trade if entry-order did not fill yet continue sell_row = data[pair][-1] @@ -869,7 +869,7 @@ class Backtesting: # Remove trade due to entry timeout expiration. return True else: - # Close additional buy order + # Close additional entry order del trade.orders[trade.orders.index(order)] if order.side == trade.exit_side: self.timedout_exit_orders += 1 @@ -882,7 +882,7 @@ class Backtesting: self, data: Dict, pair: str, row_index: int, current_time: datetime) -> Optional[Tuple]: try: # Row is treated as "current incomplete candle". - # Buy / sell signals are shifted by 1 to compensate for this. + # entry / exit signals are shifted by 1 to compensate for this. row = data[pair][row_index] except IndexError: # missing Data for one pair at the end. @@ -947,14 +947,14 @@ class Backtesting: self.dataprovider._set_dataframe_max_index(row_index) for t in list(open_trades[pair]): - # 1. Cancel expired buy/sell orders. + # 1. Cancel expired entry/exit orders. if self.check_order_cancel(t, current_time): - # Close trade due to buy timeout expiration. + # Close trade due to entry timeout expiration. open_trade_count -= 1 open_trades[pair].remove(t) self.wallets.update() - # 2. Process buys. + # 2. Process entries. # without positionstacking, we can only have one open trade per pair. # max_open_trades must be respected # don't open on the last row @@ -970,7 +970,7 @@ class Backtesting: if trade: # TODO: hacky workaround to avoid opening > max_open_trades # This emulates previous behavior - not sure if this is correct - # Prevents buying if the trade-slot was freed in this candle + # Prevents entering if the trade-slot was freed in this candle open_trade_count_start += 1 open_trade_count += 1 # logger.debug(f"{pair} - Emulate creation of new trade: {trade}.") @@ -1052,7 +1052,7 @@ class Backtesting: "No data left after adjusting for startup candles.") # Use preprocessed_tmp for date generation (the trimmed dataframe). - # Backtesting will re-trim the dataframes after buy/sell signal generation. + # Backtesting will re-trim the dataframes after entry/exit signal generation. min_date, max_date = history.get_timerange(preprocessed_tmp) logger.info(f'Backtesting with data from {min_date.strftime(DATETIME_PRINT_FORMAT)} ' f'up to {max_date.strftime(DATETIME_PRINT_FORMAT)} ' From 25c6c5e326a801556f4364d0dc5da95d6772b7af Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 24 Apr 2022 14:30:50 +0200 Subject: [PATCH 083/118] Update backtest sell terminology to exit --- freqtrade/optimize/backtesting.py | 92 +++++++++++++++--------------- tests/optimize/test_backtesting.py | 6 +- 2 files changed, 49 insertions(+), 49 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 16f94e083..5442e425b 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -178,7 +178,7 @@ class Backtesting: # Attach Wallets to Strategy baseclass strategy.wallets = self.wallets # Set stoploss_on_exchange to false for backtesting, - # since a "perfect" stoploss-sell is assumed anyway + # since a "perfect" stoploss-exit is assumed anyway # And the regular "stoploss" function would not apply to that case self.strategy.order_types['stoploss_on_exchange'] = False @@ -353,24 +353,24 @@ class Backtesting: data[pair] = df_analyzed[headers].values.tolist() if not df_analyzed.empty else [] return data - def _get_close_rate(self, row: Tuple, trade: LocalTrade, sell: ExitCheckTuple, + def _get_close_rate(self, row: Tuple, trade: LocalTrade, exit: ExitCheckTuple, trade_dur: int) -> float: """ Get close rate for backtesting result """ # Special handling if high or low hit STOP_LOSS or ROI - if sell.exit_type in (ExitType.STOP_LOSS, ExitType.TRAILING_STOP_LOSS): - return self._get_close_rate_for_stoploss(row, trade, sell, trade_dur) - elif sell.exit_type == (ExitType.ROI): - return self._get_close_rate_for_roi(row, trade, sell, trade_dur) + if exit.exit_type in (ExitType.STOP_LOSS, ExitType.TRAILING_STOP_LOSS): + return self._get_close_rate_for_stoploss(row, trade, exit, trade_dur) + elif exit.exit_type == (ExitType.ROI): + return self._get_close_rate_for_roi(row, trade, exit, trade_dur) else: return row[OPEN_IDX] - def _get_close_rate_for_stoploss(self, row: Tuple, trade: LocalTrade, sell: ExitCheckTuple, + def _get_close_rate_for_stoploss(self, row: Tuple, trade: LocalTrade, exit: ExitCheckTuple, trade_dur: int) -> float: # our stoploss was already lower than candle high, # possibly due to a cancelled trade exit. - # sell at open price. + # exit at open price. is_short = trade.is_short or False leverage = trade.leverage or 1.0 side_1 = -1 if is_short else 1 @@ -384,7 +384,7 @@ class Backtesting: # Special case: trailing triggers within same candle as trade opened. Assume most # pessimistic price movement, which is moving just enough to arm stoploss and # immediately going down to stop price. - if sell.exit_type == ExitType.TRAILING_STOP_LOSS and trade_dur == 0: + if exit.exit_type == ExitType.TRAILING_STOP_LOSS and trade_dur == 0: if ( not self.strategy.use_custom_stoploss and self.strategy.trailing_stop and self.strategy.trailing_only_offset_is_reached @@ -403,7 +403,7 @@ class Backtesting: else: assert stop_rate < row[HIGH_IDX] - # Limit lower-end to candle low to avoid sells below the low. + # Limit lower-end to candle low to avoid exits below the low. # This still remains "worst case" - but "worst realistic case". if is_short: return min(row[HIGH_IDX], stop_rate) @@ -413,7 +413,7 @@ class Backtesting: # Set close_rate to stoploss return trade.stop_loss - def _get_close_rate_for_roi(self, row: Tuple, trade: LocalTrade, sell: ExitCheckTuple, + def _get_close_rate_for_roi(self, row: Tuple, trade: LocalTrade, exit: ExitCheckTuple, trade_dur: int) -> float: is_short = trade.is_short or False leverage = trade.leverage or 1.0 @@ -438,7 +438,7 @@ class Backtesting: and roi_entry % self.timeframe_min == 0 and is_new_roi): # new ROI entry came into effect. - # use Open rate if open_rate > calculated sell rate + # use Open rate if open_rate > calculated exit rate return row[OPEN_IDX] if (trade_dur == 0 and ( @@ -461,11 +461,11 @@ class Backtesting: # ROI on opening candles with custom pricing can only # trigger if the entry was at Open or lower wick. # details: https: // github.com/freqtrade/freqtrade/issues/6261 - # If open_rate is < open, only allow sells below the close on red candles. + # If open_rate is < open, only allow exits below the close on red candles. raise ValueError("Opening candle ROI on red candles.") # Use the maximum between close_rate and low as we - # cannot sell outside of a candle. + # cannot exit outside of a candle. # Applies when a new ROI setting comes in place and the whole candle is above that. return min(max(close_rate, row[LOW_IDX]), row[HIGH_IDX]) @@ -500,7 +500,7 @@ class Backtesting: """ Rate is within candle, therefore filled""" return row[LOW_IDX] <= rate <= row[HIGH_IDX] - def _get_sell_trade_entry_for_candle(self, trade: LocalTrade, + def _get_exit_trade_entry_for_candle(self, trade: LocalTrade, row: Tuple) -> Optional[LocalTrade]: # Check if we need to adjust our current positions @@ -512,33 +512,33 @@ class Backtesting: if check_adjust_entry: trade = self._get_adjust_trade_entry_for_candle(trade, row) - sell_candle_time: datetime = row[DATE_IDX].to_pydatetime() + exit_candle_time: datetime = row[DATE_IDX].to_pydatetime() enter = row[SHORT_IDX] if trade.is_short else row[LONG_IDX] exit_ = row[ESHORT_IDX] if trade.is_short else row[ELONG_IDX] - sell = self.strategy.should_exit( - trade, row[OPEN_IDX], sell_candle_time, # type: ignore + exit_ = self.strategy.should_exit( + trade, row[OPEN_IDX], exit_candle_time, # type: ignore enter=enter, exit_=exit_, low=row[LOW_IDX], high=row[HIGH_IDX] ) - if sell.exit_flag: - trade.close_date = sell_candle_time + if exit_.exit_flag: + trade.close_date = exit_candle_time trade_dur = int((trade.close_date_utc - trade.open_date_utc).total_seconds() // 60) try: - closerate = self._get_close_rate(row, trade, sell, trade_dur) + closerate = self._get_close_rate(row, trade, exit_, trade_dur) except ValueError: return None # call the custom exit price,with default value as previous closerate current_profit = trade.calc_profit_ratio(closerate) order_type = self.strategy.order_types['exit'] - if sell.exit_type in (ExitType.EXIT_SIGNAL, ExitType.CUSTOM_EXIT): - # Custom exit pricing only for sell-signals + if exit_.exit_type in (ExitType.EXIT_SIGNAL, ExitType.CUSTOM_EXIT): + # Custom exit pricing only for exit-signals if order_type == 'limit': closerate = strategy_safe_wrapper(self.strategy.custom_exit_price, default_retval=closerate)( pair=trade.pair, trade=trade, - current_time=sell_candle_time, + current_time=exit_candle_time, proposed_rate=closerate, current_profit=current_profit) # We can't place orders lower than current low. # freqtrade does not support this in live, and the order would fill immediately @@ -553,12 +553,12 @@ class Backtesting: pair=trade.pair, trade=trade, order_type='limit', amount=trade.amount, rate=closerate, time_in_force=time_in_force, - sell_reason=sell.exit_reason, # deprecated - exit_reason=sell.exit_reason, - current_time=sell_candle_time): + sell_reason=exit_.exit_reason, # deprecated + exit_reason=exit_.exit_reason, + current_time=exit_candle_time): return None - trade.exit_reason = sell.exit_reason + trade.exit_reason = exit_.exit_reason # Checks and adds an exit tag, after checking that the length of the # row has the length for an exit tag column @@ -573,8 +573,8 @@ class Backtesting: order = Order( id=self.order_id_counter, ft_trade_id=trade.id, - order_date=sell_candle_time, - order_update_date=sell_candle_time, + order_date=exit_candle_time, + order_update_date=exit_candle_time, ft_is_open=True, ft_pair=trade.pair, order_id=str(self.order_id_counter), @@ -595,8 +595,8 @@ class Backtesting: return None - def _get_sell_trade_entry(self, trade: LocalTrade, row: Tuple) -> Optional[LocalTrade]: - sell_candle_time: datetime = row[DATE_IDX].to_pydatetime() + def _get_exit_trade_entry(self, trade: LocalTrade, row: Tuple) -> Optional[LocalTrade]: + exit_candle_time: datetime = row[DATE_IDX].to_pydatetime() if self.trading_mode == TradingMode.FUTURES: trade.funding_fees = self.exchange.calculate_funding_fees( @@ -604,20 +604,20 @@ class Backtesting: amount=trade.amount, is_short=trade.is_short, open_date=trade.open_date_utc, - close_date=sell_candle_time, + close_date=exit_candle_time, ) if self.timeframe_detail and trade.pair in self.detail_data: - sell_candle_end = sell_candle_time + timedelta(minutes=self.timeframe_min) + exit_candle_end = exit_candle_time + timedelta(minutes=self.timeframe_min) detail_data = self.detail_data[trade.pair] detail_data = detail_data.loc[ - (detail_data['date'] >= sell_candle_time) & - (detail_data['date'] < sell_candle_end) + (detail_data['date'] >= exit_candle_time) & + (detail_data['date'] < exit_candle_end) ].copy() if len(detail_data) == 0: # Fall back to "regular" data if no detail data was found for this candle - return self._get_sell_trade_entry_for_candle(trade, row) + return self._get_exit_trade_entry_for_candle(trade, row) detail_data.loc[:, 'enter_long'] = row[LONG_IDX] detail_data.loc[:, 'exit_long'] = row[ELONG_IDX] detail_data.loc[:, 'enter_short'] = row[SHORT_IDX] @@ -627,14 +627,14 @@ class Backtesting: headers = ['date', 'open', 'high', 'low', 'close', 'enter_long', 'exit_long', 'enter_short', 'exit_short', 'enter_tag', 'exit_tag'] for det_row in detail_data[headers].values.tolist(): - res = self._get_sell_trade_entry_for_candle(trade, det_row) + res = self._get_exit_trade_entry_for_candle(trade, det_row) if res: return res return None else: - return self._get_sell_trade_entry_for_candle(trade, row) + return self._get_exit_trade_entry_for_candle(trade, row) def get_valid_price_and_stake( self, pair: str, row: Tuple, propose_rate: float, stake_amount: Optional[float], @@ -815,11 +815,11 @@ class Backtesting: if trade.open_order_id and trade.nr_of_successful_entries == 0: # Ignore trade if entry-order did not fill yet continue - sell_row = data[pair][-1] + exit_row = data[pair][-1] - trade.close_date = sell_row[DATE_IDX].to_pydatetime() + trade.close_date = exit_row[DATE_IDX].to_pydatetime() trade.exit_reason = ExitType.FORCE_EXIT.value - trade.close(sell_row[OPEN_IDX], show_msg=False) + trade.close(exit_row[OPEN_IDX], show_msg=False) LocalTrade.close_bt_trade(trade) # Deepcopy object to have wallets update correctly trade1 = deepcopy(trade) @@ -985,18 +985,18 @@ class Backtesting: LocalTrade.add_bt_trade(trade) self.wallets.update() - # 4. Create sell orders (if any) + # 4. Create exit orders (if any) if not trade.open_order_id: - self._get_sell_trade_entry(trade, row) # Place sell order if necessary + self._get_exit_trade_entry(trade, row) # Place exit order if necessary - # 5. Process sell orders. + # 5. Process exit orders. order = trade.select_order(trade.exit_side, is_open=True) if order and self._get_order_filled(order.price, row): trade.open_order_id = None trade.close_date = current_time trade.close(order.price, show_msg=False) - # logger.debug(f"{pair} - Backtesting sell {trade}") + # logger.debug(f"{pair} - Backtesting exit {trade}") open_trade_count -= 1 open_trades[pair].remove(trade) LocalTrade.close_bt_trade(trade) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 797d3bafa..4d32a7516 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -714,7 +714,7 @@ def test_backtest__get_sell_trade_entry(default_conf, fee, mocker) -> None: ) # No data available. - res = backtesting._get_sell_trade_entry(trade, row_sell) + res = backtesting._get_exit_trade_entry(trade, row_sell) assert res is not None assert res.exit_reason == ExitType.ROI.value assert res.close_date_utc == datetime(2020, 1, 1, 5, 0, tzinfo=timezone.utc) @@ -727,13 +727,13 @@ def test_backtest__get_sell_trade_entry(default_conf, fee, mocker) -> None: [], columns=['date', 'open', 'high', 'low', 'close', 'enter_long', 'exit_long', 'enter_short', 'exit_short', 'long_tag', 'short_tag', 'exit_tag']) - res = backtesting._get_sell_trade_entry(trade, row) + res = backtesting._get_exit_trade_entry(trade, row) assert res is None # Assign backtest-detail data backtesting.detail_data[pair] = row_detail - res = backtesting._get_sell_trade_entry(trade, row_sell) + res = backtesting._get_exit_trade_entry(trade, row_sell) assert res is not None assert res.exit_reason == ExitType.ROI.value # Sell at minute 3 (not available above!) From 46ac46a5d3a9629b7f9d3ce8c704f693336b811f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Apr 2022 03:17:23 +0000 Subject: [PATCH 084/118] Bump ccxt from 1.79.81 to 1.80.61 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.79.81 to 1.80.61. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/exchanges.cfg) - [Commits](https://github.com/ccxt/ccxt/compare/1.79.81...1.80.61) --- updated-dependencies: - dependency-name: ccxt dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 571d1892c..de14b9f2c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ numpy==1.22.3 pandas==1.4.2 pandas-ta==0.3.14b -ccxt==1.79.81 +ccxt==1.80.61 # Pin cryptography for now due to rust build errors with piwheels cryptography==36.0.2 aiohttp==3.8.1 From 5bfa2186a77f774cb3043edabafd56745dab3a53 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Apr 2022 03:17:29 +0000 Subject: [PATCH 085/118] Bump pytest from 7.1.1 to 7.1.2 Bumps [pytest](https://github.com/pytest-dev/pytest) from 7.1.1 to 7.1.2. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/7.1.1...7.1.2) --- updated-dependencies: - dependency-name: pytest dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 4fb4456f0..7f914875b 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -9,7 +9,7 @@ flake8==4.0.1 flake8-tidy-imports==4.6.0 mypy==0.942 pre-commit==2.18.1 -pytest==7.1.1 +pytest==7.1.2 pytest-asyncio==0.18.3 pytest-cov==3.0.0 pytest-mock==3.7.0 From eee9fbb6690fa23749a4cf71940ec2e83bcb59c0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Apr 2022 03:17:38 +0000 Subject: [PATCH 086/118] Bump types-python-dateutil from 2.8.11 to 2.8.12 Bumps [types-python-dateutil](https://github.com/python/typeshed) from 2.8.11 to 2.8.12. - [Release notes](https://github.com/python/typeshed/releases) - [Commits](https://github.com/python/typeshed/commits) --- updated-dependencies: - dependency-name: types-python-dateutil dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 4fb4456f0..4f296b321 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -28,4 +28,4 @@ types-requests==2.27.19 types-tabulate==0.8.7 # Extensions to datetime library -types-python-dateutil==2.8.11 +types-python-dateutil==2.8.12 From 9b39c835867120a69041c36712700a45eab49694 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Apr 2022 03:17:44 +0000 Subject: [PATCH 087/118] Bump mkdocs-material from 8.2.9 to 8.2.10 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 8.2.9 to 8.2.10. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/8.2.9...8.2.10) --- updated-dependencies: - dependency-name: mkdocs-material dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- docs/requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index 8d03a38c3..c6b683e8f 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,5 +1,5 @@ mkdocs==1.3.0 -mkdocs-material==8.2.9 +mkdocs-material==8.2.10 mdx_truly_sane_lists==1.2 pymdown-extensions==9.3 jinja2==3.1.1 From 399be6f4e54566cf4e8e7d5d2f4719c9d0a9a765 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Apr 2022 03:17:53 +0000 Subject: [PATCH 088/118] Bump types-requests from 2.27.19 to 2.27.20 Bumps [types-requests](https://github.com/python/typeshed) from 2.27.19 to 2.27.20. - [Release notes](https://github.com/python/typeshed/releases) - [Commits](https://github.com/python/typeshed/commits) --- updated-dependencies: - dependency-name: types-requests dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 4fb4456f0..be23d14bf 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -24,7 +24,7 @@ nbconvert==6.5.0 # mypy types types-cachetools==5.0.1 types-filelock==3.2.5 -types-requests==2.27.19 +types-requests==2.27.20 types-tabulate==0.8.7 # Extensions to datetime library From b4afbb0b0ac2bdb96a6b653bc44240c3e5669112 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Apr 2022 04:54:53 +0000 Subject: [PATCH 089/118] Bump pymdown-extensions from 9.3 to 9.4 Bumps [pymdown-extensions](https://github.com/facelessuser/pymdown-extensions) from 9.3 to 9.4. - [Release notes](https://github.com/facelessuser/pymdown-extensions/releases) - [Commits](https://github.com/facelessuser/pymdown-extensions/compare/9.3...9.4) --- updated-dependencies: - dependency-name: pymdown-extensions dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- docs/requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index c6b683e8f..97be17243 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,5 +1,5 @@ mkdocs==1.3.0 mkdocs-material==8.2.10 mdx_truly_sane_lists==1.2 -pymdown-extensions==9.3 +pymdown-extensions==9.4 jinja2==3.1.1 From 562e36c3ec20ce5d05c44fac78eb7e2ac71fb7a8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 25 Apr 2022 07:01:27 +0200 Subject: [PATCH 090/118] Remove Interface V1 support --- freqtrade/resolvers/strategy_resolver.py | 18 +++++++++------ freqtrade/strategy/interface.py | 28 ++++-------------------- 2 files changed, 15 insertions(+), 31 deletions(-) diff --git a/freqtrade/resolvers/strategy_resolver.py b/freqtrade/resolvers/strategy_resolver.py index 0265ad6c3..44d590b67 100644 --- a/freqtrade/resolvers/strategy_resolver.py +++ b/freqtrade/resolvers/strategy_resolver.py @@ -217,15 +217,19 @@ class StrategyResolver(IResolver): raise OperationalException( "`populate_exit_trend` or `populate_sell_trend` must be implemented.") - strategy._populate_fun_len = len(getfullargspec(strategy.populate_indicators).args) - strategy._buy_fun_len = len(getfullargspec(strategy.populate_buy_trend).args) - strategy._sell_fun_len = len(getfullargspec(strategy.populate_sell_trend).args) + _populate_fun_len = len(getfullargspec(strategy.populate_indicators).args) + _buy_fun_len = len(getfullargspec(strategy.populate_buy_trend).args) + _sell_fun_len = len(getfullargspec(strategy.populate_sell_trend).args) if any(x == 2 for x in [ - strategy._populate_fun_len, - strategy._buy_fun_len, - strategy._sell_fun_len + _populate_fun_len, + _buy_fun_len, + _sell_fun_len ]): - strategy.INTERFACE_VERSION = 1 + raise OperationalException( + "Strategy Interface v1 is no longer supported. " + "Please update your strategy to implement " + "`populate_indicators`, `populate_entry_trend` and `populate_exit_trend` " + "with the metadata argument. ") return strategy @staticmethod diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index afcc1aa99..0ec3895bc 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -3,7 +3,6 @@ IStrategy interface This module defines the interface to apply for strategies """ import logging -import warnings from abc import ABC, abstractmethod from datetime import datetime, timedelta, timezone from typing import Dict, List, Optional, Tuple, Union @@ -44,14 +43,11 @@ class IStrategy(ABC, HyperStrategyMixin): """ # Strategy interface version # Default to version 2 - # Version 1 is the initial interface without metadata dict + # Version 1 is the initial interface without metadata dict - deprecated and no longer supported. # Version 2 populate_* include metadata dict # Version 3 - First version with short and leverage support INTERFACE_VERSION: int = 3 - _populate_fun_len: int = 0 - _buy_fun_len: int = 0 - _sell_fun_len: int = 0 _ft_params_from_file: Dict # associated minimal roi minimal_roi: Dict = {} @@ -1090,12 +1086,7 @@ class IStrategy(ABC, HyperStrategyMixin): dataframe = _create_and_merge_informative_pair( self, dataframe, metadata, inf_data, populate_fn) - if self._populate_fun_len == 2: - warnings.warn("deprecated - check out the Sample strategy to see " - "the current function headers!", DeprecationWarning) - return self.populate_indicators(dataframe) # type: ignore - else: - return self.populate_indicators(dataframe, metadata) + return self.populate_indicators(dataframe, metadata) def advise_entry(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ @@ -1109,12 +1100,7 @@ class IStrategy(ABC, HyperStrategyMixin): logger.debug(f"Populating enter signals for pair {metadata.get('pair')}.") - if self._buy_fun_len == 2: - warnings.warn("deprecated - check out the Sample strategy to see " - "the current function headers!", DeprecationWarning) - df = self.populate_buy_trend(dataframe) # type: ignore - else: - df = self.populate_entry_trend(dataframe, metadata) + df = self.populate_entry_trend(dataframe, metadata) if 'enter_long' not in df.columns: df = df.rename({'buy': 'enter_long', 'buy_tag': 'enter_tag'}, axis='columns') @@ -1129,14 +1115,8 @@ class IStrategy(ABC, HyperStrategyMixin): currently traded pair :return: DataFrame with exit column """ - logger.debug(f"Populating exit signals for pair {metadata.get('pair')}.") - if self._sell_fun_len == 2: - warnings.warn("deprecated - check out the Sample strategy to see " - "the current function headers!", DeprecationWarning) - df = self.populate_sell_trend(dataframe) # type: ignore - else: - df = self.populate_exit_trend(dataframe, metadata) + df = self.populate_exit_trend(dataframe, metadata) if 'exit_long' not in df.columns: df = df.rename({'sell': 'exit_long'}, axis='columns') return df From ec2582a4aed614dd1da552e9b76562e09f41c3d8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 25 Apr 2022 07:02:09 +0200 Subject: [PATCH 091/118] Update tests to no longer use Strategy V1 --- tests/optimize/test_backtesting.py | 27 ++++----- tests/strategy/strats/legacy_strategy_v1.py | 60 +----------------- tests/strategy/test_interface.py | 2 +- tests/strategy/test_strategy_loading.py | 67 ++------------------- 4 files changed, 21 insertions(+), 135 deletions(-) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 4d32a7516..d7ee4a042 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -500,7 +500,7 @@ def test_backtesting_pairlist_list(default_conf, mocker, caplog, testdatadir, ti Backtesting(default_conf) # Multiple strategies - default_conf['strategy_list'] = [CURRENT_TEST_STRATEGY, 'TestStrategyLegacyV1'] + default_conf['strategy_list'] = [CURRENT_TEST_STRATEGY, 'StrategyTestV2'] with pytest.raises(OperationalException, match='PrecisionFilter not allowed for backtesting multiple strategies.'): Backtesting(default_conf) @@ -1198,7 +1198,7 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir): '--disable-max-market-positions', '--strategy-list', CURRENT_TEST_STRATEGY, - 'TestStrategyLegacyV1', + 'StrategyTestV2', ] args = get_args(args) start_backtesting(args) @@ -1221,14 +1221,13 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir): 'up to 2017-11-14 22:58:00 (0 days).', 'Parameter --enable-position-stacking detected ...', f'Running backtesting for Strategy {CURRENT_TEST_STRATEGY}', - 'Running backtesting for Strategy TestStrategyLegacyV1', + 'Running backtesting for Strategy StrategyTestV2', ] for line in exists: assert log_has(line, caplog) -@pytest.mark.filterwarnings("ignore:deprecated") def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdatadir, capsys): default_conf.update({ "use_exit_signal": True, @@ -1310,7 +1309,7 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat '--breakdown', 'day', '--strategy-list', CURRENT_TEST_STRATEGY, - 'TestStrategyLegacyV1', + 'StrategyTestV2', ] args = get_args(args) start_backtesting(args) @@ -1327,7 +1326,7 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat 'up to 2017-11-14 22:58:00 (0 days).', 'Parameter --enable-position-stacking detected ...', f'Running backtesting for Strategy {CURRENT_TEST_STRATEGY}', - 'Running backtesting for Strategy TestStrategyLegacyV1', + 'Running backtesting for Strategy StrategyTestV2', ] for line in exists: @@ -1592,7 +1591,7 @@ def test_backtest_start_multi_strat_caching(default_conf, mocker, caplog, testda min_backtest_date = now - timedelta(weeks=4) load_backtest_metadata = MagicMock(return_value={ 'StrategyTestV2': {'run_id': '1', 'backtest_start_time': now.timestamp()}, - 'TestStrategyLegacyV1': {'run_id': run_id, 'backtest_start_time': start_time.timestamp()} + 'StrategyTestV3': {'run_id': run_id, 'backtest_start_time': start_time.timestamp()} }) load_backtest_stats = MagicMock(side_effect=[ { @@ -1601,9 +1600,9 @@ def test_backtest_start_multi_strat_caching(default_conf, mocker, caplog, testda 'strategy_comparison': [{'key': 'StrategyTestV2'}] }, { - 'metadata': {'TestStrategyLegacyV1': {'run_id': '2'}}, - 'strategy': {'TestStrategyLegacyV1': {}}, - 'strategy_comparison': [{'key': 'TestStrategyLegacyV1'}] + 'metadata': {'StrategyTestV3': {'run_id': '2'}}, + 'strategy': {'StrategyTestV3': {}}, + 'strategy_comparison': [{'key': 'StrategyTestV3'}] } ]) mocker.patch('pathlib.Path.glob', return_value=[ @@ -1627,7 +1626,7 @@ def test_backtest_start_multi_strat_caching(default_conf, mocker, caplog, testda '--cache', cache, '--strategy-list', 'StrategyTestV2', - 'TestStrategyLegacyV1', + 'StrategyTestV3', ] args = get_args(args) start_backtesting(args) @@ -1649,7 +1648,7 @@ def test_backtest_start_multi_strat_caching(default_conf, mocker, caplog, testda assert backtestmock.call_count == 2 exists = [ 'Running backtesting for Strategy StrategyTestV2', - 'Running backtesting for Strategy TestStrategyLegacyV1', + 'Running backtesting for Strategy StrategyTestV3', 'Ignoring max_open_trades (--disable-max-market-positions was used) ...', 'Backtesting with data from 2017-11-14 21:17:00 up to 2017-11-14 22:58:00 (0 days).', ] @@ -1657,12 +1656,12 @@ def test_backtest_start_multi_strat_caching(default_conf, mocker, caplog, testda assert backtestmock.call_count == 0 exists = [ 'Reusing result of previous backtest for StrategyTestV2', - 'Reusing result of previous backtest for TestStrategyLegacyV1', + 'Reusing result of previous backtest for StrategyTestV3', ] else: exists = [ 'Reusing result of previous backtest for StrategyTestV2', - 'Running backtesting for Strategy TestStrategyLegacyV1', + 'Running backtesting for Strategy StrategyTestV3', 'Ignoring max_open_trades (--disable-max-market-positions was used) ...', 'Backtesting with data from 2017-11-14 21:17:00 up to 2017-11-14 22:58:00 (0 days).', ] diff --git a/tests/strategy/strats/legacy_strategy_v1.py b/tests/strategy/strats/legacy_strategy_v1.py index bad2aa40d..a1fe4ee15 100644 --- a/tests/strategy/strats/legacy_strategy_v1.py +++ b/tests/strategy/strats/legacy_strategy_v1.py @@ -1,85 +1,29 @@ - -# --- Do not remove these libs --- -# Add your lib to import here -import talib.abstract as ta from pandas import DataFrame from freqtrade.strategy import IStrategy -# -------------------------------- - -# This class is a sample. Feel free to customize it. +# Dummy strategy - no longer loads but raises an exception. class TestStrategyLegacyV1(IStrategy): - """ - This is a test strategy using the legacy function headers, which will be - removed in a future update. - Please do not use this as a template, but refer to user_data/strategy/sample_strategy.py - for a uptodate version of this template. - """ - # Minimal ROI designed for the strategy. - # This attribute will be overridden if the config file contains "minimal_roi" minimal_roi = { "40": 0.0, "30": 0.01, "20": 0.02, "0": 0.04 } - - # Optimal stoploss designed for the strategy - # This attribute will be overridden if the config file contains "stoploss" stoploss = -0.10 timeframe = '5m' def populate_indicators(self, dataframe: DataFrame) -> DataFrame: - """ - Adds several different TA indicators to the given DataFrame - - Performance Note: For the best performance be frugal on the number of indicators - you are using. Let uncomment only the indicator you are using in your strategies - or your hyperopt configuration, otherwise you will waste your memory and CPU usage. - """ - - # Momentum Indicator - # ------------------------------------ - - # ADX - dataframe['adx'] = ta.ADX(dataframe) - - # TEMA - Triple Exponential Moving Average - dataframe['tema'] = ta.TEMA(dataframe, timeperiod=9) return dataframe def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame: - """ - Based on TA indicators, populates the buy signal for the given dataframe - :param dataframe: DataFrame - :return: DataFrame with buy column - """ - dataframe.loc[ - ( - (dataframe['adx'] > 30) & - (dataframe['tema'] > dataframe['tema'].shift(1)) & - (dataframe['volume'] > 0) - ), - 'buy'] = 1 return dataframe def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame: - """ - Based on TA indicators, populates the sell signal for the given dataframe - :param dataframe: DataFrame - :return: DataFrame with buy column - """ - dataframe.loc[ - ( - (dataframe['adx'] > 70) & - (dataframe['tema'] < dataframe['tema'].shift(1)) & - (dataframe['volume'] > 0) - ), - 'sell'] = 1 + return dataframe diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index a86d69135..ea81fe968 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -686,7 +686,7 @@ def test_is_pair_locked(default_conf): def test_is_informative_pairs_callback(default_conf): - default_conf.update({'strategy': 'TestStrategyLegacyV1'}) + default_conf.update({'strategy': 'StrategyTestV2'}) strategy = StrategyResolver.load_strategy(default_conf) # Should return empty # Uses fallback to base implementation diff --git a/tests/strategy/test_strategy_loading.py b/tests/strategy/test_strategy_loading.py index e74a2a022..85f7961ed 100644 --- a/tests/strategy/test_strategy_loading.py +++ b/tests/strategy/test_strategy_loading.py @@ -100,7 +100,7 @@ def test_load_strategy_noname(default_conf): @pytest.mark.filterwarnings("ignore:deprecated") -@pytest.mark.parametrize('strategy_name', ['StrategyTestV2', 'TestStrategyLegacyV1']) +@pytest.mark.parametrize('strategy_name', ['StrategyTestV2']) def test_strategy_pre_v3(result, default_conf, strategy_name): default_conf.update({'strategy': strategy_name}) @@ -346,40 +346,6 @@ def test_strategy_override_use_exit_profit_only(caplog, default_conf): assert log_has("Override strategy 'exit_profit_only' with value in config file: True.", caplog) -@pytest.mark.filterwarnings("ignore:deprecated") -def test_deprecate_populate_indicators(result, default_conf): - default_location = Path(__file__).parent / "strats" - default_conf.update({'strategy': 'TestStrategyLegacyV1', - 'strategy_path': default_location}) - strategy = StrategyResolver.load_strategy(default_conf) - with warnings.catch_warnings(record=True) as w: - # Cause all warnings to always be triggered. - warnings.simplefilter("always") - indicators = strategy.advise_indicators(result, {'pair': 'ETH/BTC'}) - assert len(w) == 1 - assert issubclass(w[-1].category, DeprecationWarning) - assert "deprecated - check out the Sample strategy to see the current function headers!" \ - in str(w[-1].message) - - with warnings.catch_warnings(record=True) as w: - # Cause all warnings to always be triggered. - warnings.simplefilter("always") - strategy.advise_entry(indicators, {'pair': 'ETH/BTC'}) - assert len(w) == 1 - assert issubclass(w[-1].category, DeprecationWarning) - assert "deprecated - check out the Sample strategy to see the current function headers!" \ - in str(w[-1].message) - - with warnings.catch_warnings(record=True) as w: - # Cause all warnings to always be triggered. - warnings.simplefilter("always") - strategy.advise_exit(indicators, {'pair': 'ETH_BTC'}) - assert len(w) == 1 - assert issubclass(w[-1].category, DeprecationWarning) - assert "deprecated - check out the Sample strategy to see the current function headers!" \ - in str(w[-1].message) - - @pytest.mark.filterwarnings("ignore:deprecated") def test_missing_implements(default_conf, caplog): @@ -438,33 +404,14 @@ def test_missing_implements(default_conf, caplog): StrategyResolver.load_strategy(default_conf) -@pytest.mark.filterwarnings("ignore:deprecated") -def test_call_deprecated_function(result, default_conf, caplog): +def test_call_deprecated_function(default_conf): default_location = Path(__file__).parent / "strats" del default_conf['timeframe'] default_conf.update({'strategy': 'TestStrategyLegacyV1', 'strategy_path': default_location}) - strategy = StrategyResolver.load_strategy(default_conf) - metadata = {'pair': 'ETH/BTC'} - - # Make sure we are using a legacy function - assert strategy._populate_fun_len == 2 - assert strategy._buy_fun_len == 2 - assert strategy._sell_fun_len == 2 - assert strategy.INTERFACE_VERSION == 1 - assert strategy.timeframe == '5m' - - indicator_df = strategy.advise_indicators(result, metadata=metadata) - assert isinstance(indicator_df, DataFrame) - assert 'adx' in indicator_df.columns - - enterdf = strategy.advise_entry(result, metadata=metadata) - assert isinstance(enterdf, DataFrame) - assert 'enter_long' in enterdf.columns - - exitdf = strategy.advise_exit(result, metadata=metadata) - assert isinstance(exitdf, DataFrame) - assert 'exit_long' in exitdf + with pytest.raises(OperationalException, + match=r"Strategy Interface v1 is no longer supported.*"): + StrategyResolver.load_strategy(default_conf) def test_strategy_interface_versioning(result, default_conf): @@ -472,10 +419,6 @@ def test_strategy_interface_versioning(result, default_conf): strategy = StrategyResolver.load_strategy(default_conf) metadata = {'pair': 'ETH/BTC'} - # Make sure we are using a legacy function - assert strategy._populate_fun_len == 3 - assert strategy._buy_fun_len == 3 - assert strategy._sell_fun_len == 3 assert strategy.INTERFACE_VERSION == 2 indicator_df = strategy.advise_indicators(result, metadata=metadata) From 9bb0f1f675d211a3dd95359b6129c10628dcc958 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 25 Apr 2022 07:09:08 +0200 Subject: [PATCH 092/118] Move legacy strategy to "broken strats" folder --- tests/commands/test_commands.py | 16 ++++++++-------- tests/rpc/test_rpc_apiserver.py | 1 - .../{ => broken_strats}/legacy_strategy_v1.py | 1 + tests/strategy/test_strategy_loading.py | 9 ++++----- 4 files changed, 13 insertions(+), 14 deletions(-) rename tests/strategy/strats/{ => broken_strats}/legacy_strategy_v1.py (97%) diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index d1f54ad52..37eeda86a 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -859,8 +859,8 @@ def test_start_list_strategies(capsys): # pargs['config'] = None start_list_strategies(pargs) captured = capsys.readouterr() - assert "TestStrategyLegacyV1" in captured.out - assert "legacy_strategy_v1.py" not in captured.out + assert "StrategyTestV2" in captured.out + assert "strategy_test_v2.py" not in captured.out assert CURRENT_TEST_STRATEGY in captured.out # Test regular output @@ -874,8 +874,8 @@ def test_start_list_strategies(capsys): # pargs['config'] = None start_list_strategies(pargs) captured = capsys.readouterr() - assert "TestStrategyLegacyV1" in captured.out - assert "legacy_strategy_v1.py" in captured.out + assert "StrategyTestV2" in captured.out + assert "strategy_test_v2.py" in captured.out assert CURRENT_TEST_STRATEGY in captured.out # Test color output @@ -888,8 +888,8 @@ def test_start_list_strategies(capsys): # pargs['config'] = None start_list_strategies(pargs) captured = capsys.readouterr() - assert "TestStrategyLegacyV1" in captured.out - assert "legacy_strategy_v1.py" in captured.out + assert "StrategyTestV2" in captured.out + assert "strategy_test_v2.py" in captured.out assert CURRENT_TEST_STRATEGY in captured.out assert "LOAD FAILED" in captured.out # Recursive @@ -907,8 +907,8 @@ def test_start_list_strategies(capsys): # pargs['config'] = None start_list_strategies(pargs) captured = capsys.readouterr() - assert "TestStrategyLegacyV1" in captured.out - assert "legacy_strategy_v1.py" in captured.out + assert "StrategyTestV2" in captured.out + assert "strategy_test_v2.py" in captured.out assert "StrategyTestV2" in captured.out assert "TestStrategyNoImplements" in captured.out assert str(Path("broken_strats/broken_futures_strategies.py")) in captured.out diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index af8361571..4910213b4 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -1389,7 +1389,6 @@ def test_api_strategies(botclient): 'StrategyTestV2', 'StrategyTestV3', 'StrategyTestV3Futures', - 'TestStrategyLegacyV1', ]} diff --git a/tests/strategy/strats/legacy_strategy_v1.py b/tests/strategy/strats/broken_strats/legacy_strategy_v1.py similarity index 97% rename from tests/strategy/strats/legacy_strategy_v1.py rename to tests/strategy/strats/broken_strats/legacy_strategy_v1.py index a1fe4ee15..f3b8c2696 100644 --- a/tests/strategy/strats/legacy_strategy_v1.py +++ b/tests/strategy/strats/broken_strats/legacy_strategy_v1.py @@ -1,3 +1,4 @@ +# type: ignore from pandas import DataFrame from freqtrade.strategy import IStrategy diff --git a/tests/strategy/test_strategy_loading.py b/tests/strategy/test_strategy_loading.py index 85f7961ed..3ed1eb0ce 100644 --- a/tests/strategy/test_strategy_loading.py +++ b/tests/strategy/test_strategy_loading.py @@ -1,6 +1,5 @@ # pragma pylint: disable=missing-docstring, protected-access, C0103 import logging -import warnings from base64 import urlsafe_b64encode from pathlib import Path @@ -35,7 +34,7 @@ def test_search_all_strategies_no_failed(): directory = Path(__file__).parent / "strats" strategies = StrategyResolver.search_all_objects(directory, enum_failed=False) assert isinstance(strategies, list) - assert len(strategies) == 6 + assert len(strategies) == 5 assert isinstance(strategies[0], dict) @@ -43,10 +42,10 @@ def test_search_all_strategies_with_failed(): directory = Path(__file__).parent / "strats" strategies = StrategyResolver.search_all_objects(directory, enum_failed=True) assert isinstance(strategies, list) - assert len(strategies) == 7 + assert len(strategies) == 6 # with enum_failed=True search_all_objects() shall find 2 good strategies # and 1 which fails to load - assert len([x for x in strategies if x['class'] is not None]) == 6 + assert len([x for x in strategies if x['class'] is not None]) == 5 assert len([x for x in strategies if x['class'] is None]) == 1 @@ -405,7 +404,7 @@ def test_missing_implements(default_conf, caplog): def test_call_deprecated_function(default_conf): - default_location = Path(__file__).parent / "strats" + default_location = Path(__file__).parent / "strats/broken_strats/" del default_conf['timeframe'] default_conf.update({'strategy': 'TestStrategyLegacyV1', 'strategy_path': default_location}) From ad6e5c53120053556b4cd0e19581a853e3e02432 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 25 Apr 2022 07:41:51 +0200 Subject: [PATCH 093/118] Test informative fallback again --- tests/strategy/strats/strategy_test_v2.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/tests/strategy/strats/strategy_test_v2.py b/tests/strategy/strats/strategy_test_v2.py index 8996b227a..85ff856e1 100644 --- a/tests/strategy/strats/strategy_test_v2.py +++ b/tests/strategy/strats/strategy_test_v2.py @@ -56,19 +56,6 @@ class StrategyTestV2(IStrategy): # By default this strategy does not use Position Adjustments position_adjustment_enable = False - def informative_pairs(self): - """ - Define additional, informative pair/interval combinations to be cached from the exchange. - These pair/interval combinations are non-tradeable, unless they are part - of the whitelist as well. - For more information, please consult the documentation - :return: List of tuples in the format (pair, interval) - Sample: return [("ETH/USDT", "5m"), - ("BTC/USDT", "15m"), - ] - """ - return [] - def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Adds several different TA indicators to the given DataFrame From 86b3aac9ba8a0db45c986fa8ebda7bb19292fe33 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 25 Apr 2022 08:38:02 +0200 Subject: [PATCH 094/118] Fix FTX not fetching the very latest data --- freqtrade/exchange/exchange.py | 9 ++++++--- freqtrade/exchange/ftx.py | 1 + 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index ab063762b..2eb705b53 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -64,6 +64,7 @@ class Exchange: "ohlcv_params": {}, "ohlcv_candle_limit": 500, "ohlcv_partial_candle": True, + "ohlcv_require_since": False, # Check https://github.com/ccxt/ccxt/issues/10767 for removal of ohlcv_volume_currency "ohlcv_volume_currency": "base", # "base" or "quote" "tickers_have_quoteVolume": True, @@ -1710,7 +1711,8 @@ class Exchange: def _build_coroutine(self, pair: str, timeframe: str, candle_type: CandleType, since_ms: Optional[int]) -> Coroutine: - if not since_ms and self.required_candle_call_count > 1: + if (not since_ms + and (self._ft_has["ohlcv_require_since"] or self.required_candle_call_count > 1)): # Multiple calls for one pair - to get more history one_call = timeframe_to_msecs(timeframe) * self.ohlcv_candle_limit(timeframe) move_to = one_call * self.required_candle_call_count @@ -1829,17 +1831,18 @@ class Exchange: pair, timeframe, since_ms, s ) params = deepcopy(self._ft_has.get('ohlcv_params', {})) + candle_limit = self.ohlcv_candle_limit(timeframe) if candle_type != CandleType.SPOT: params.update({'price': candle_type}) if candle_type != CandleType.FUNDING_RATE: data = await self._api_async.fetch_ohlcv( pair, timeframe=timeframe, since=since_ms, - limit=self.ohlcv_candle_limit(timeframe), params=params) + limit=candle_limit, params=params) else: # Funding rate data = await self._api_async.fetch_funding_rate_history( pair, since=since_ms, - limit=self.ohlcv_candle_limit(timeframe)) + limit=candle_limit) # Convert funding rate to candle pattern data = [[x['timestamp'], x['fundingRate'], 0, 0, 0, 0] for x in data] # Some exchanges sort OHLCV in ASC order and others in DESC. diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index f20aab138..d2dcf84a6 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -20,6 +20,7 @@ class Ftx(Exchange): _ft_has: Dict = { "stoploss_on_exchange": True, "ohlcv_candle_limit": 1500, + "ohlcv_require_since": True, "ohlcv_volume_currency": "quote", "mark_ohlcv_price": "index", "mark_ohlcv_timeframe": "1h", From 7b02114ad2826bf4260ec98d47d9cd9e9a191d78 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 25 Apr 2022 09:49:51 +0200 Subject: [PATCH 095/118] Restrict trading pairs with too low precision closes #6606 --- freqtrade/exchange/exchange.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 2eb705b53..d4741bd64 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -369,6 +369,9 @@ class Exchange: return ( market.get('quote', None) is not None and market.get('base', None) is not None + and (self.precisionMode != TICK_SIZE + # Too low precision will falsify calculations + or market.get('precision', {}).get('price', None) > 1e-11) and ((self.trading_mode == TradingMode.SPOT and self.market_is_spot(market)) or (self.trading_mode == TradingMode.MARGIN and self.market_is_margin(market)) or (self.trading_mode == TradingMode.FUTURES and self.market_is_future(market))) From 6d576bc02d4474b29de678052b3a420d2aa2cc4d Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 25 Apr 2022 10:15:07 +0200 Subject: [PATCH 096/118] Check pre-commit verison updates --- .github/workflows/ci.yml | 21 ++++++++++++--- .pre-commit-config.yaml | 7 +++++ build_helpers/pre_commit_update.py | 42 ++++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 3 deletions(-) create mode 100644 build_helpers/pre_commit_update.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1902a6c45..35c237837 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -265,6 +265,21 @@ jobs: details: Test Failed webhookUrl: ${{ secrets.DISCORD_WEBHOOK }} + mypy_version_check: + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v3 + with: + python-version: 3.9 + + - name: pre-commit dependencies + run: | + pip install pyaml + python build_helpers/pre_commit_update.py + docs_check: runs-on: ubuntu-20.04 steps: @@ -277,7 +292,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v3 with: - python-version: 3.8 + python-version: 3.9 - name: Documentation build run: | @@ -304,7 +319,7 @@ jobs: # Notify only once - when CI completes (and after deploy) in case it's successfull notify-complete: - needs: [ build_linux, build_macos, build_windows, docs_check ] + needs: [ build_linux, build_macos, build_windows, docs_check, mypy_version_check ] runs-on: ubuntu-20.04 steps: @@ -325,7 +340,7 @@ jobs: webhookUrl: ${{ secrets.DISCORD_WEBHOOK }} deploy: - needs: [ build_linux, build_macos, build_windows, docs_check ] + needs: [ build_linux, build_macos, build_windows, docs_check, mypy_version_check ] runs-on: ubuntu-20.04 if: (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'release') && github.repository == 'freqtrade/freqtrade' diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 316baf0e3..f223f0b9b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,6 +11,13 @@ repos: rev: "v0.942" hooks: - id: mypy + args: [ freqtrade ] + additional_dependencies: + - types-cachetools==5.0.1 + - types-filelock==3.2.5 + - types-requests==2.27.19 + - types-tabulate==0.8.7 + - types-python-dateutil==2.8.11 # stages: [push] - repo: https://github.com/pycqa/isort diff --git a/build_helpers/pre_commit_update.py b/build_helpers/pre_commit_update.py new file mode 100644 index 000000000..8724d8ade --- /dev/null +++ b/build_helpers/pre_commit_update.py @@ -0,0 +1,42 @@ +# File used in CI to ensure pre-commit dependencies are kept uptodate. + +import sys +from pathlib import Path + +import yaml + + +pre_commit_file = Path('.pre-commit-config.yaml') +require_dev = Path('requirements-dev.txt') + +with require_dev.open('r') as rfile: + requirements = rfile.readlines() + +# Extract types only +type_reqs = [r.strip('\n') for r in requirements if r.startswith('types-')] + +with pre_commit_file.open('r') as file: + f = yaml.load(file, Loader=yaml.FullLoader) + + +mypy_repo = [repo for repo in f['repos'] if repo['repo'] + == 'https://github.com/pre-commit/mirrors-mypy'] + +hooks = mypy_repo[0]['hooks'][0]['additional_dependencies'] + +errors = [] +for hook in hooks: + if hook not in type_reqs: + errors.append(f"{hook} is missing in requirements-dev.txt.") + +for req in type_reqs: + if req not in hooks: + errors.append(f"{req} is missing in pre-config file.") + + +if errors: + for e in errors: + print(e) + sys.exit(1) + +sys.exit(0) From fc118d0e95d38e7f5a655c79fc3de41ab9f2f537 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 25 Apr 2022 10:19:31 +0200 Subject: [PATCH 097/118] Re-align dependencies --- .pre-commit-config.yaml | 4 ++-- requirements-dev.txt | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f223f0b9b..0dd343bb8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,9 +15,9 @@ repos: additional_dependencies: - types-cachetools==5.0.1 - types-filelock==3.2.5 - - types-requests==2.27.19 + - types-requests==2.27.20 - types-tabulate==0.8.7 - - types-python-dateutil==2.8.11 + - types-python-dateutil==2.8.12 # stages: [push] - repo: https://github.com/pycqa/isort diff --git a/requirements-dev.txt b/requirements-dev.txt index b0210d64a..c4fe366a5 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -26,6 +26,4 @@ types-cachetools==5.0.1 types-filelock==3.2.5 types-requests==2.27.20 types-tabulate==0.8.7 - -# Extensions to datetime library types-python-dateutil==2.8.12 From 4143ebbeae53eb27706cf15573f4f36b905ac8e0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 25 Apr 2022 10:51:11 +0200 Subject: [PATCH 098/118] Add CAGR calculation to backtesting --- docs/backtesting.md | 2 ++ freqtrade/data/btanalysis.py | 11 +++++++++++ freqtrade/optimize/optimize_reports.py | 4 +++- tests/data/test_btanalysis.py | 27 +++++++++++++++++++------- 4 files changed, 36 insertions(+), 8 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index f732068f1..a0a304400 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -299,6 +299,7 @@ A backtesting result will look like that: | 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 | @@ -388,6 +389,7 @@ It contains some useful key metrics about performance of your strategy on backte | 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 | | | | diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 8abcc6747..206a6f5f3 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -553,3 +553,14 @@ def calculate_csum(trades: pd.DataFrame, starting_balance: float = 0) -> Tuple[f csum_max = csum_df['sum'].max() + starting_balance return csum_min, csum_max + + +def calculate_cagr(days_passed: int, starting_balance: float, final_balance: float) -> float: + """ + Calculate CAGR + :param days_passed: Days passed between start and ending balance + :param starting_balance: Starting balance + :param final_balance: Final balance to calculate CAGR against + :return: CAGR + """ + return (final_balance / starting_balance) ** (1 / (days_passed / 365)) - 1 diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 0ceb3a411..e8bd035d1 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -9,7 +9,7 @@ from pandas import DataFrame, to_datetime from tabulate import tabulate from freqtrade.constants import DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN, UNLIMITED_STAKE_AMOUNT -from freqtrade.data.btanalysis import (calculate_csum, calculate_market_change, +from freqtrade.data.btanalysis import (calculate_cagr, calculate_csum, calculate_market_change, calculate_max_drawdown) from freqtrade.misc import (decimals_per_coin, file_dump_joblib, file_dump_json, get_backtest_metadata_filename, round_coin_value) @@ -446,6 +446,7 @@ def generate_strategy_stats(pairlist: List[str], 'profit_total_abs': results['profit_abs'].sum(), 'profit_total_long_abs': results.loc[~results['is_short'], 'profit_abs'].sum(), 'profit_total_short_abs': results.loc[results['is_short'], 'profit_abs'].sum(), + 'cagr': calculate_cagr(backtest_days, start_balance, content['final_balance']), 'backtest_start': min_date.strftime(DATETIME_PRINT_FORMAT), 'backtest_start_ts': int(min_date.timestamp() * 1000), 'backtest_end': max_date.strftime(DATETIME_PRINT_FORMAT), @@ -746,6 +747,7 @@ def text_table_add_metrics(strat_results: Dict) -> str: ('Absolute profit ', round_coin_value(strat_results['profit_total_abs'], strat_results['stake_currency'])), ('Total profit %', f"{strat_results['profit_total']:.2%}"), + ('CAGR %', f"{strat_results['cagr']:.2%}"), ('Trades per day', strat_results['trades_per_day']), ('Avg. daily profit %', f"{(strat_results['profit_total'] / strat_results['backtest_days']):.2%}"), diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index 2b53e4900..eaf703b2d 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -8,13 +8,13 @@ from pandas import DataFrame, DateOffset, Timestamp, to_datetime from freqtrade.configuration import TimeRange from freqtrade.constants import LAST_BT_RESULT_FN -from freqtrade.data.btanalysis import (BT_DATA_COLUMNS, analyze_trade_parallelism, calculate_csum, - calculate_market_change, calculate_max_drawdown, - 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_backtest_metadata, load_trades, - load_trades_from_db) +from freqtrade.data.btanalysis import (BT_DATA_COLUMNS, analyze_trade_parallelism, calculate_cagr, + calculate_csum, calculate_market_change, + calculate_max_drawdown, 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_backtest_metadata, load_trades, load_trades_from_db) from freqtrade.data.history import load_data, load_pair_history from freqtrade.exceptions import OperationalException from tests.conftest import CURRENT_TEST_STRATEGY, create_mock_trades @@ -336,6 +336,19 @@ def test_calculate_csum(testdatadir): csum_min, csum_max = calculate_csum(DataFrame()) +@pytest.mark.parametrize('start,end,days, expected', [ + (64900, 176000, 3 * 365, 0.3945), + (64900, 176000, 365, 1.7119), + (1000, 1000, 365, 0.0), + (1000, 1500, 365, 0.5), + (1000, 1500, 100, 3.3927), # sub year + (0.01000000, 0.01762792, 120, 4.6087), # sub year BTC values +]) +def test_calculate_cagr(start, end, days, expected): + + assert round(calculate_cagr(days, start, end), 4) == expected + + def test_calculate_max_drawdown2(): values = [0.011580, 0.010048, 0.011340, 0.012161, 0.010416, 0.010009, 0.020024, -0.024662, -0.022350, 0.020496, -0.029859, -0.030511, 0.010041, 0.010872, From 500fdc2759639d9275ab82bfbcaaa01e48017eae Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 25 Apr 2022 11:12:35 +0200 Subject: [PATCH 099/118] run mypy also against tests --- .github/workflows/ci.yml | 4 ++-- .pre-commit-config.yaml | 2 +- freqtrade/strategy/informative_decorator.py | 2 +- freqtrade/strategy/interface.py | 2 +- setup.cfg | 4 ++++ 5 files changed, 9 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 35c237837..5bafe9cb8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -100,7 +100,7 @@ jobs: - name: Mypy run: | - mypy freqtrade scripts + mypy freqtrade scripts tests - name: Discord notification uses: rjstone/discord-webhook-notify@v1 @@ -255,7 +255,7 @@ jobs: - name: Mypy run: | - mypy freqtrade scripts + mypy freqtrade scripts tests - name: Discord notification uses: rjstone/discord-webhook-notify@v1 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0dd343bb8..d980fc4e9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ repos: rev: "v0.942" hooks: - id: mypy - args: [ freqtrade ] + args: [ freqtrade, scripts, tests ] additional_dependencies: - types-cachetools==5.0.1 - types-filelock==3.2.5 diff --git a/freqtrade/strategy/informative_decorator.py b/freqtrade/strategy/informative_decorator.py index 0dd5320cd..7dfdf5a8c 100644 --- a/freqtrade/strategy/informative_decorator.py +++ b/freqtrade/strategy/informative_decorator.py @@ -23,7 +23,7 @@ class InformativeData: def informative(timeframe: str, asset: str = '', fmt: Optional[Union[str, Callable[[Any], str]]] = None, *, - candle_type: Optional[CandleType] = None, + candle_type: Optional[Union[CandleType, str]] = None, ffill: bool = True) -> Callable[[PopulateIndicators], PopulateIndicators]: """ A decorator for populate_indicators_Nn(self, dataframe, metadata), allowing these functions to diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 0ec3895bc..300010b83 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -110,7 +110,7 @@ class IStrategy(ABC, HyperStrategyMixin): # Class level variables (intentional) containing # the dataprovider (dp) (access to other candles, historic data, ...) # and wallets - access to the current balance. - dp: Optional[DataProvider] + dp: DataProvider wallets: Optional[Wallets] = None # Filled from configuration stake_currency: str diff --git a/setup.cfg b/setup.cfg index a33ceda1f..edbd320c3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -53,6 +53,10 @@ exclude = [mypy] ignore_missing_imports = True warn_unused_ignores = True +exclude = (?x)( + ^build_helpers\.py$ + ) + [mypy-tests.*] ignore_errors = True From 2b3f68396081a41861797ef0a4abc3c08221765f Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 25 Apr 2022 11:23:45 +0200 Subject: [PATCH 100/118] Update pre-commit to exclude build-helpers --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d980fc4e9..2170b704a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ repos: rev: "v0.942" hooks: - id: mypy - args: [ freqtrade, scripts, tests ] + exclude: build_helpers additional_dependencies: - types-cachetools==5.0.1 - types-filelock==3.2.5 From 580da21ddaa3e9fa8a0016434bc0ffbaf9794276 Mon Sep 17 00:00:00 2001 From: froggleston Date: Mon, 25 Apr 2022 10:31:19 +0100 Subject: [PATCH 101/118] Move df append to pd concat --- freqtrade/optimize/backtesting.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) mode change 100644 => 100755 freqtrade/optimize/backtesting.py diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py old mode 100644 new mode 100755 index 5442e425b..217e6ff54 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -10,6 +10,7 @@ from datetime import datetime, timedelta, timezone from typing import Any, Dict, List, Optional, Tuple from numpy import nan +import pandas as pd from pandas import DataFrame from freqtrade import constants @@ -1093,7 +1094,7 @@ class Backtesting: for t, v in pairresults.open_date.items(): allinds = pairdf.loc[(pairdf['date'] < v)] signal_inds = allinds.iloc[[-1]] - signal_candles_only_df = signal_candles_only_df.append(signal_inds) + signal_candles_only_df = pd.concat([signal_candles_only_df, signal_inds]) signal_candles_only[pair] = signal_candles_only_df From 431c539cbdf3e207d9a7afdc299a14f3b245f547 Mon Sep 17 00:00:00 2001 From: froggleston Date: Mon, 25 Apr 2022 10:42:24 +0100 Subject: [PATCH 102/118] Fix isort import order --- freqtrade/optimize/backtesting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 217e6ff54..27e14ba93 100755 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -9,8 +9,8 @@ from copy import deepcopy from datetime import datetime, timedelta, timezone from typing import Any, Dict, List, Optional, Tuple -from numpy import nan import pandas as pd +from numpy import nan from pandas import DataFrame from freqtrade import constants From 44000ae0b390ce4a4fa48e4fc588863fdafe8c87 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 25 Apr 2022 17:37:25 +0200 Subject: [PATCH 103/118] Fix CAGR missing for old results --- freqtrade/optimize/optimize_reports.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index e8bd035d1..dd058aff4 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -747,7 +747,7 @@ def text_table_add_metrics(strat_results: Dict) -> str: ('Absolute profit ', round_coin_value(strat_results['profit_total_abs'], strat_results['stake_currency'])), ('Total profit %', f"{strat_results['profit_total']:.2%}"), - ('CAGR %', f"{strat_results['cagr']:.2%}"), + ('CAGR %', f"{strat_results['cagr']:.2%}" if 'cagr' in strat_results else 'N/A'), ('Trades per day', strat_results['trades_per_day']), ('Avg. daily profit %', f"{(strat_results['profit_total'] / strat_results['backtest_days']):.2%}"), From 4cccf31a3eb005a2560a0479b42aa8ef2f4b5939 Mon Sep 17 00:00:00 2001 From: naveen <172697+naveensrinivasan@users.noreply.github.com> Date: Tue, 26 Apr 2022 01:07:59 +0000 Subject: [PATCH 104/118] chore: Set permissions for GitHub actions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Restrict the GitHub token permissions only to the required ones; this way, even if the attackers will succeed in compromising your workflow, they won’t be able to do much. - Included permissions for the action. https://github.com/ossf/scorecard/blob/main/docs/checks.md#token-permissions https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions https://docs.github.com/en/actions/using-jobs/assigning-permissions-to-jobs [Keeping your GitHub Actions and workflows secure Part 1: Preventing pwn requests](https://securitylab.github.com/research/github-actions-preventing-pwn-requests/) Signed-off-by: naveen <172697+naveensrinivasan@users.noreply.github.com> --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5bafe9cb8..932649f61 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -309,6 +309,9 @@ jobs: webhookUrl: ${{ secrets.DISCORD_WEBHOOK }} cleanup-prior-runs: + permissions: + actions: write # for rokroskar/workflow-run-cleanup-action to obtain workflow name & cancel it + contents: read # for rokroskar/workflow-run-cleanup-action to obtain branch runs-on: ubuntu-20.04 steps: - name: Cleanup previous runs on this branch From 6d99222320f05de1f7c2607f0cbe1beb8a38c29f Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Tue, 26 Apr 2022 09:39:15 +0300 Subject: [PATCH 105/118] Add 'exit_tag' parameter to 'custom_exit_price' callback. --- docs/strategy-callbacks.md | 2 +- freqtrade/freqtradebot.py | 3 ++- freqtrade/optimize/backtesting.py | 3 ++- freqtrade/strategy/interface.py | 3 ++- .../templates/subtemplates/strategy_methods_advanced.j2 | 3 ++- tests/test_freqtradebot.py | 6 +++--- 6 files changed, 12 insertions(+), 8 deletions(-) diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index 7ec600a58..005f94155 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -376,7 +376,7 @@ class AwesomeStrategy(IStrategy): def custom_exit_price(self, pair: str, trade: Trade, current_time: datetime, proposed_rate: float, - current_profit: float, **kwargs) -> float: + current_profit: float, exit_tag: Optional[str, **kwargs) -> float: dataframe, last_updated = self.dp.get_analyzed_dataframe(pair=pair, timeframe=self.timeframe) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 57d7cac3c..68623c748 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1373,7 +1373,8 @@ class FreqtradeBot(LoggingMixin): default_retval=proposed_limit_rate)( pair=trade.pair, trade=trade, current_time=datetime.now(timezone.utc), - proposed_rate=proposed_limit_rate, current_profit=current_profit) + proposed_rate=proposed_limit_rate, current_profit=current_profit, + exit_tag=exit_check.exit_reason) limit = self.get_valid_price(custom_exit_price, proposed_limit_rate) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 27e14ba93..21e124e72 100755 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -540,7 +540,8 @@ class Backtesting: default_retval=closerate)( pair=trade.pair, trade=trade, current_time=exit_candle_time, - proposed_rate=closerate, current_profit=current_profit) + proposed_rate=closerate, current_profit=current_profit, + exit_tag=exit_.exit_reason) # We can't place orders lower than current low. # freqtrade does not support this in live, and the order would fill immediately if trade.is_short: diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 300010b83..4f9e91b56 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -355,7 +355,7 @@ class IStrategy(ABC, HyperStrategyMixin): def custom_exit_price(self, pair: str, trade: Trade, current_time: datetime, proposed_rate: float, - current_profit: float, **kwargs) -> float: + current_profit: float, exit_tag: Optional[str], **kwargs) -> float: """ Custom exit price logic, returning the new exit price. @@ -368,6 +368,7 @@ class IStrategy(ABC, HyperStrategyMixin): :param current_time: datetime object, containing the current datetime :param proposed_rate: Rate, calculated based on pricing settings in exit_pricing. :param current_profit: Current profit (as ratio), calculated based on current_rate. + :param exit_tag: Exit reason. :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. :return float: New exit price value if provided """ diff --git a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 index d5e2ea8ce..3fa36d506 100644 --- a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 +++ b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 @@ -32,7 +32,7 @@ def custom_entry_price(self, pair: str, current_time: 'datetime', proposed_rate: def custom_exit_price(self, pair: str, trade: 'Trade', current_time: 'datetime', proposed_rate: float, - current_profit: float, **kwargs) -> float: + current_profit: float, exit_tag: Optional[str, **kwargs) -> float: """ Custom exit price logic, returning the new exit price. @@ -45,6 +45,7 @@ def custom_exit_price(self, pair: str, trade: 'Trade', :param current_time: datetime object, containing the current datetime :param proposed_rate: Rate, calculated based on pricing settings in exit_pricing. :param current_profit: Current profit (as ratio), calculated based on current_rate. + :param exit_tag: Exit reason. :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. :return float: New exit price value if provided """ diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 3737c7c05..84d3c3324 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -3221,7 +3221,7 @@ def test_execute_trade_exit_custom_exit_price( freqtrade.execute_trade_exit( trade=trade, limit=ticker_usdt_sell_up()['ask' if is_short else 'bid'], - exit_check=ExitCheckTuple(exit_type=ExitType.EXIT_SIGNAL) + exit_check=ExitCheckTuple(exit_type=ExitType.EXIT_SIGNAL, exit_reason='foo') ) # Sell price must be different to default bid price @@ -3249,8 +3249,8 @@ def test_execute_trade_exit_custom_exit_price( 'profit_ratio': profit_ratio, 'stake_currency': 'USDT', 'fiat_currency': 'USD', - 'sell_reason': ExitType.EXIT_SIGNAL.value, - 'exit_reason': ExitType.EXIT_SIGNAL.value, + 'sell_reason': 'foo', + 'exit_reason': 'foo', 'open_date': ANY, 'close_date': ANY, 'close_rate': ANY, From 108f11b1d796dc8a371250f2dbb028161bad5739 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 27 Apr 2022 06:42:56 +0200 Subject: [PATCH 106/118] Fix docs typos --- docs/strategy-callbacks.md | 2 +- freqtrade/templates/subtemplates/strategy_methods_advanced.j2 | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index 005f94155..563b5a2cb 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -376,7 +376,7 @@ class AwesomeStrategy(IStrategy): def custom_exit_price(self, pair: str, trade: Trade, current_time: datetime, proposed_rate: float, - current_profit: float, exit_tag: Optional[str, **kwargs) -> float: + current_profit: float, exit_tag: Optional[str], **kwargs) -> float: dataframe, last_updated = self.dp.get_analyzed_dataframe(pair=pair, timeframe=self.timeframe) diff --git a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 index 3fa36d506..ed40ef509 100644 --- a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 +++ b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 @@ -32,7 +32,7 @@ def custom_entry_price(self, pair: str, current_time: 'datetime', proposed_rate: def custom_exit_price(self, pair: str, trade: 'Trade', current_time: 'datetime', proposed_rate: float, - current_profit: float, exit_tag: Optional[str, **kwargs) -> float: + current_profit: float, exit_tag: Optional[str], **kwargs) -> float: """ Custom exit price logic, returning the new exit price. From ad7fbfab1bacbfaa3778595d2f073411286f9619 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 27 Apr 2022 13:27:33 +0200 Subject: [PATCH 107/118] Slightly improved styling --- freqtrade/rpc/telegram.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index c2531fec3..1a9be4503 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -943,7 +943,7 @@ class Telegram(RPCHandler): else: fiat_currency = self._config.get('fiat_display_currency', '') try: - statlist, head, fiat_profit_sum = self._rpc._rpc_status_table( + statlist, _, _ = self._rpc._rpc_status_table( self._config['stake_currency'], fiat_currency) except RPCException: self._send_msg(msg='No open trade found.') From 30c9dc697530fda7add40cf7cae941df4cb06076 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 27 Apr 2022 13:53:11 +0200 Subject: [PATCH 108/118] Fix exit-signa being assigned when tag is set but no signal is present. --- freqtrade/optimize/backtesting.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 21e124e72..210eab39b 100755 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -54,6 +54,11 @@ ESHORT_IDX = 8 # Exit short ENTER_TAG_IDX = 9 EXIT_TAG_IDX = 10 +# Every change to this headers list must evaluate further usages of the resulting tuple +# and eventually change the constants for indexes at the top +HEADERS = ['date', 'open', 'high', 'low', 'close', 'enter_long', 'exit_long', + 'enter_short', 'exit_short', 'enter_tag', 'exit_tag'] + class Backtesting: """ @@ -305,10 +310,7 @@ class Backtesting: :param processed: a processed dictionary with format {pair, data}, which gets cleared to optimize memory usage! """ - # Every change to this headers list must evaluate further usages of the resulting tuple - # and eventually change the constants for indexes at the top - headers = ['date', 'open', 'high', 'low', 'close', 'enter_long', 'exit_long', - 'enter_short', 'exit_short', 'enter_tag', 'exit_tag'] + data: Dict = {} self.progress.init_step(BacktestState.CONVERT, len(processed)) @@ -320,7 +322,7 @@ class Backtesting: if not pair_data.empty: # Cleanup from prior runs - pair_data.drop(headers[5:] + ['buy', 'sell'], axis=1, errors='ignore') + pair_data.drop(HEADERS[5:] + ['buy', 'sell'], axis=1, errors='ignore') df_analyzed = self.strategy.advise_exit( self.strategy.advise_entry(pair_data, {'pair': pair}), @@ -339,7 +341,7 @@ class Backtesting: # To avoid using data from future, we use entry/exit signals shifted # from the previous candle - for col in headers[5:]: + for col in HEADERS[5:]: tag_col = col in ('enter_tag', 'exit_tag') if col in df_analyzed.columns: df_analyzed.loc[:, col] = df_analyzed.loc[:, col].replace( @@ -351,7 +353,7 @@ class Backtesting: # Convert from Pandas to list for performance reasons # (Looping Pandas is slow.) - data[pair] = df_analyzed[headers].values.tolist() if not df_analyzed.empty else [] + data[pair] = df_analyzed[HEADERS].values.tolist() if not df_analyzed.empty else [] return data def _get_close_rate(self, row: Tuple, trade: LocalTrade, exit: ExitCheckTuple, @@ -515,10 +517,10 @@ class Backtesting: exit_candle_time: datetime = row[DATE_IDX].to_pydatetime() enter = row[SHORT_IDX] if trade.is_short else row[LONG_IDX] - exit_ = row[ESHORT_IDX] if trade.is_short else row[ELONG_IDX] + exit_sig = row[ESHORT_IDX] if trade.is_short else row[ELONG_IDX] exit_ = self.strategy.should_exit( trade, row[OPEN_IDX], exit_candle_time, # type: ignore - enter=enter, exit_=exit_, + enter=enter, exit_=exit_sig, low=row[LOW_IDX], high=row[HIGH_IDX] ) @@ -568,6 +570,7 @@ class Backtesting: len(row) > EXIT_TAG_IDX and row[EXIT_TAG_IDX] is not None and len(row[EXIT_TAG_IDX]) > 0 + and exit_.exit_type in (ExitType.EXIT_SIGNAL,) ): trade.exit_reason = row[EXIT_TAG_IDX] @@ -626,9 +629,7 @@ class Backtesting: detail_data.loc[:, 'exit_short'] = row[ESHORT_IDX] detail_data.loc[:, 'enter_tag'] = row[ENTER_TAG_IDX] detail_data.loc[:, 'exit_tag'] = row[EXIT_TAG_IDX] - headers = ['date', 'open', 'high', 'low', 'close', 'enter_long', 'exit_long', - 'enter_short', 'exit_short', 'enter_tag', 'exit_tag'] - for det_row in detail_data[headers].values.tolist(): + for det_row in detail_data[HEADERS].values.tolist(): res = self._get_exit_trade_entry_for_candle(trade, det_row) if res: return res From 2c0a7c5d74df52a4ff166bc23515d318b5d08466 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 27 Apr 2022 17:01:23 +0200 Subject: [PATCH 109/118] Don't call interest_rate and isolated_liq twice --- freqtrade/freqtradebot.py | 12 ------------ tests/test_freqtradebot.py | 12 +++++------- 2 files changed, 5 insertions(+), 19 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 68623c748..c82f2c8fe 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -664,16 +664,6 @@ class FreqtradeBot(LoggingMixin): amount = safe_value_fallback(order, 'filled', 'amount') enter_limit_filled_price = safe_value_fallback(order, 'average', 'price') - # TODO: this might be unnecessary, as we're calling it in update_trade_state. - isolated_liq = self.exchange.get_liquidation_price( - leverage=leverage, - pair=pair, - amount=amount, - open_rate=enter_limit_filled_price, - is_short=is_short - ) - interest_rate = self.exchange.get_interest_rate() - # Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker') base_currency = self.exchange.get_pair_base_currency(pair) @@ -702,8 +692,6 @@ class FreqtradeBot(LoggingMixin): timeframe=timeframe_to_minutes(self.config['timeframe']), leverage=leverage, is_short=is_short, - interest_rate=interest_rate, - liquidation_price=isolated_liq, trading_mode=self.trading_mode, funding_fees=funding_fees ) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 84d3c3324..89fe88a2c 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -717,12 +717,12 @@ def test_process_informative_pairs_added(default_conf_usdt, ticker_usdt, mocker) (True, 'spot', 'gateio', None, 0.0, None), (False, 'spot', 'okx', None, 0.0, None), (True, 'spot', 'okx', None, 0.0, None), - (True, 'futures', 'binance', 'isolated', 0.0, 11.89108910891089), - (False, 'futures', 'binance', 'isolated', 0.0, 8.070707070707071), + (True, 'futures', 'binance', 'isolated', 0.0, 11.88151815181518), + (False, 'futures', 'binance', 'isolated', 0.0, 8.080471380471382), (True, 'futures', 'gateio', 'isolated', 0.0, 11.87413417771621), (False, 'futures', 'gateio', 'isolated', 0.0, 8.085708510208207), - (True, 'futures', 'binance', 'isolated', 0.05, 11.796534653465345), - (False, 'futures', 'binance', 'isolated', 0.05, 8.167171717171717), + (True, 'futures', 'binance', 'isolated', 0.05, 11.7874422442244), + (False, 'futures', 'binance', 'isolated', 0.05, 8.17644781144781), (True, 'futures', 'gateio', 'isolated', 0.05, 11.7804274688304), (False, 'futures', 'gateio', 'isolated', 0.05, 8.181423084697796), (True, 'futures', 'okx', 'isolated', 0.0, 11.87413417771621), @@ -845,6 +845,7 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order, assert trade.open_order_id is None assert trade.open_rate == 10 assert trade.stake_amount == round(order['price'] * order['filled'] / leverage, 8) + assert pytest.approx(trade.liquidation_price) == liq_price # In case of rejected or expired order and partially filled order['status'] = 'expired' @@ -932,8 +933,6 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order, assert trade.open_rate_requested == 10 # In case of custom entry price not float type - freqtrade.exchange.get_maintenance_ratio_and_amt = MagicMock(return_value=(0.01, 0.01)) - freqtrade.exchange.name = exchange_name order['status'] = 'open' order['id'] = '5568' freqtrade.strategy.custom_entry_price = lambda **kwargs: "string price" @@ -946,7 +945,6 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order, trade.is_short = is_short assert trade assert trade.open_rate_requested == 10 - assert trade.liquidation_price == liq_price # In case of too high stake amount From 220927289d2419046efcaf7bae1499effa537a54 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 27 Apr 2022 19:10:04 +0200 Subject: [PATCH 110/118] Update documentation to highlight futures supported exchanges --- README.md | 8 ++++++++ docs/index.md | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/README.md b/README.md index 679dbcab0..cad39f9ac 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,14 @@ Please read the [exchange specific notes](docs/exchanges.md) to learn about even - [X] [OKX](https://okx.com/) (Former OKEX) - [ ] [potentially many others](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_ +### Experimentally, freqtrade also supports futures on the following exchanges + +- [X] [Binance](https://www.binance.com/) +- [X] [Gate.io](https://www.gate.io/ref/6266643) +- [X] [OKX](https://okx.com/). + +Please make sure to read the [exchange specific notes](docs/exchanges.md), as well as the [trading with leverage](docs/leverage.md) documentation before diving in. + ### Community tested Exchanges confirmed working by the community: diff --git a/docs/index.md b/docs/index.md index 2aa80c240..e0a88a381 100644 --- a/docs/index.md +++ b/docs/index.md @@ -51,6 +51,14 @@ Please read the [exchange specific notes](exchanges.md) to learn about eventual, - [X] [OKX](https://okx.com/) (Former OKEX) - [ ] [potentially many others through ccxt](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_ +### Experimentally, freqtrade also supports futures on the following exchanges: + +- [X] [Binance](https://www.binance.com/) +- [X] [Gate.io](https://www.gate.io/ref/6266643) +- [X] [OKX](https://okx.com/). + +Please make sure to read the [exchange specific notes](exchanges.md), as well as the [trading with leverage](leverage.md) documentation before diving in. + ### Community tested Exchanges confirmed working by the community: From 46855221aab34d0c0687474c9e8a1af08cfa9916 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 27 Apr 2022 19:58:19 +0200 Subject: [PATCH 111/118] Fix rounding issue with contract-sized pairs for dry-run orders --- freqtrade/exchange/exchange.py | 4 +++- freqtrade/freqtradebot.py | 1 - 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index d4741bd64..b12751fff 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -785,7 +785,9 @@ class Exchange: rate: float, leverage: float, params: Dict = {}, stop_loss: bool = False) -> Dict[str, Any]: order_id = f'dry_run_{side}_{datetime.now().timestamp()}' - _amount = self.amount_to_precision(pair, amount) + # Rounding here must respect to contract sizes + _amount = self._contracts_to_amount( + pair, self.amount_to_precision(pair, self._amount_to_contracts(pair, amount))) dry_order: Dict[str, Any] = { 'id': order_id, 'symbol': pair, diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index c82f2c8fe..7c20a7f60 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -585,7 +585,6 @@ class FreqtradeBot(LoggingMixin): Executes a limit buy for the given pair :param pair: pair for which we want to create a LIMIT_BUY :param stake_amount: amount of stake-currency for the pair - :param leverage: amount of leverage applied to this trade :return: True if a buy order is created, false if it fails. """ time_in_force = self.strategy.order_time_in_force['entry'] From ca49821df011c8ebb7ec8586c08daf675d210bd4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 28 Apr 2022 06:29:14 +0200 Subject: [PATCH 112/118] Fix race condition for loop --- freqtrade/exchange/exchange.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index d4741bd64..8ecccbce0 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -9,6 +9,7 @@ import logging from copy import deepcopy from datetime import datetime, timedelta, timezone from math import ceil +from threading import Lock from typing import Any, Coroutine, Dict, List, Literal, Optional, Tuple, Union import arrow @@ -96,6 +97,9 @@ class Exchange: self._markets: Dict = {} self._trading_fees: Dict[str, Any] = {} self._leverage_tiers: Dict[str, List[Dict]] = {} + # Lock event loop. This is necessary to avoid race-conditions when using force* commands + # Due to funding fee fetching. + self._loop_lock = Lock() self.loop = asyncio.new_event_loop() asyncio.set_event_loop(self.loop) self._config: Dict = {} @@ -1775,7 +1779,8 @@ class Exchange: async def gather_stuff(): return await asyncio.gather(*input_coro, return_exceptions=True) - results = self.loop.run_until_complete(gather_stuff()) + with self._loop_lock: + results = self.loop.run_until_complete(gather_stuff()) for res in results: if isinstance(res, Exception): @@ -2032,9 +2037,10 @@ class Exchange: if not self.exchange_has("fetchTrades"): raise OperationalException("This exchange does not support downloading Trades.") - return self.loop.run_until_complete( - self._async_get_trade_history(pair=pair, since=since, - until=until, from_id=from_id)) + with self._loop_lock: + return self.loop.run_until_complete( + self._async_get_trade_history(pair=pair, since=since, + until=until, from_id=from_id)) @retrier def _get_funding_fees_from_exchange(self, pair: str, since: Union[datetime, int]) -> float: From 1e835896415191469a369c30cf5848d96004d7e3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 28 Apr 2022 06:59:03 +0200 Subject: [PATCH 113/118] Fix hyperopt --- freqtrade/optimize/hyperopt.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 3ae975ca7..1dafb483c 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -468,6 +468,7 @@ class Hyperopt: self.backtesting.exchange._api = None self.backtesting.exchange._api_async = None self.backtesting.exchange.loop = None # type: ignore + self.backtesting.exchange._loop_lock = None # type: ignore # self.backtesting.exchange = None # type: ignore self.backtesting.pairlists = None # type: ignore From 2ef1181e16b8d2389f51687d3be67b78b26e01f8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 28 Apr 2022 07:33:30 +0200 Subject: [PATCH 114/118] Simplify trade __repr__ --- freqtrade/persistence/models.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index a9c07f12c..2cacc06e2 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -429,12 +429,10 @@ class LocalTrade(): def __repr__(self): open_since = self.open_date.strftime(DATETIME_PRINT_FORMAT) if self.is_open else 'closed' - leverage = self.leverage or 1.0 - is_short = self.is_short or False return ( f'Trade(id={self.id}, pair={self.pair}, amount={self.amount:.8f}, ' - f'is_short={is_short}, leverage={leverage}, ' + f'is_short={self.is_short or False}, leverage={self.leverage or 1.0}, ' f'open_rate={self.open_rate:.8f}, open_since={open_since})' ) From 64072f76b9760679bebb24b66cd4dc52ba85d40f Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 28 Apr 2022 14:40:47 +0200 Subject: [PATCH 115/118] Don't fail scheduled ci tasks due to notification --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5bafe9cb8..0a87e2c81 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -321,6 +321,8 @@ jobs: notify-complete: needs: [ build_linux, build_macos, build_windows, docs_check, mypy_version_check ] runs-on: ubuntu-20.04 + # Discord notification can't handle schedule events + if: (github.event_name != 'schedule') steps: - name: Check user permission From 4c95996069dc161fa3c6aaba8cdc6ad9629a6250 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 28 Apr 2022 14:50:50 +0200 Subject: [PATCH 116/118] Add Permissions for notify-complete job --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 932649f61..9e7ebfc6f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -324,6 +324,8 @@ jobs: notify-complete: needs: [ build_linux, build_macos, build_windows, docs_check, mypy_version_check ] runs-on: ubuntu-20.04 + permissions: + repository-projects: read steps: - name: Check user permission From cb5c3316d1e3dd63658de539c94eb559d91e4e73 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 28 Apr 2022 19:43:52 +0200 Subject: [PATCH 117/118] Simplify log output --- freqtrade/exchange/exchange.py | 4 ++-- tests/exchange/test_exchange.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 6bb4eb446..82dcacb51 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -171,7 +171,7 @@ class Exchange: self._api_async = self._init_ccxt( exchange_config, ccxt_async, ccxt_kwargs=ccxt_async_config) - logger.info('Using Exchange "%s"', self.name) + logger.info(f'Using Exchange "{self.name}"') if validate: # Check if timeframe is available @@ -559,7 +559,7 @@ class Exchange: # Therefore we also show that. raise OperationalException( f"The ccxt library does not provide the list of timeframes " - f"for the exchange \"{self.name}\" and this exchange " + f"for the exchange {self.name} and this exchange " f"is therefore not supported. ccxt fetchOHLCV: {self.exchange_has('fetchOHLCV')}") if timeframe and (timeframe not in self.timeframes): diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index a6918b6d4..689ffa4ce 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -909,7 +909,7 @@ def test_validate_timeframes_emulated_ohlcv_1(default_conf, mocker): mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') with pytest.raises(OperationalException, match=r'The ccxt library does not provide the list of timeframes ' - r'for the exchange ".*" and this exchange ' + r'for the exchange .* and this exchange ' r'is therefore not supported. *'): Exchange(default_conf) @@ -930,7 +930,7 @@ def test_validate_timeframes_emulated_ohlcvi_2(default_conf, mocker): mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') with pytest.raises(OperationalException, match=r'The ccxt library does not provide the list of timeframes ' - r'for the exchange ".*" and this exchange ' + r'for the exchange .* and this exchange ' r'is therefore not supported. *'): Exchange(default_conf) From d1a61f9c615fb7e5d9717c126d9280bccf3e30ec Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 28 Apr 2022 20:05:19 +0200 Subject: [PATCH 118/118] Don't start futures backtest if leverage-tiers don't contain pair --- freqtrade/optimize/backtesting.py | 8 ++++++++ tests/optimize/test_backtesting.py | 33 ++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 210eab39b..0f816f295 100755 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -269,10 +269,18 @@ class Backtesting: candle_type=CandleType.from_string(self.exchange._ft_has["mark_ohlcv_price"]) ) # Combine data to avoid combining the data per trade. + unavailable_pairs = [] for pair in self.pairlists.whitelist: + if pair not in self.exchange._leverage_tiers: + unavailable_pairs.append(pair) + continue self.futures_data[pair] = funding_rates_dict[pair].merge( mark_rates_dict[pair], on='date', how="inner", suffixes=["_fund", "_mark"]) + if unavailable_pairs: + raise OperationalException( + f"Pairs {', '.join(unavailable_pairs)} got no leverage tiers available. " + "It is therefore impossible to backtest with this pair at the moment.") else: self.futures_data = {} diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index d7ee4a042..a51e1b654 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -1341,6 +1341,39 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat assert 'STRATEGY SUMMARY' in captured.out +@pytest.mark.filterwarnings("ignore:deprecated") +def test_backtest_start_futures_noliq(default_conf_usdt, mocker, + caplog, testdatadir, capsys): + # Tests detail-data loading + default_conf_usdt.update({ + "trading_mode": "futures", + "margin_mode": "isolated", + "use_exit_signal": True, + "exit_profit_only": False, + "exit_profit_offset": 0.0, + "ignore_roi_if_entry_signal": False, + "strategy": CURRENT_TEST_STRATEGY, + }) + patch_exchange(mocker) + + mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist', + PropertyMock(return_value=['HULUMULU/USDT', 'XRP/USDT'])) + # mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', backtestmock) + + patched_configuration_load_config_file(mocker, default_conf_usdt) + + args = [ + 'backtesting', + '--config', 'config.json', + '--datadir', str(testdatadir), + '--strategy-path', str(Path(__file__).parents[1] / 'strategy/strats'), + '--timeframe', '1h', + ] + args = get_args(args) + with pytest.raises(OperationalException, match=r"Pairs .* got no leverage tiers available\."): + start_backtesting(args) + + @pytest.mark.filterwarnings("ignore:deprecated") def test_backtest_start_nomock_futures(default_conf_usdt, mocker, caplog, testdatadir, capsys):