From d3cb211283ced68d082cfdbdac12f3d2ab90d63b Mon Sep 17 00:00:00 2001 From: th0rntwig Date: Thu, 18 Aug 2022 14:44:49 +0200 Subject: [PATCH 01/96] Add inlier metric computation --- freqtrade/freqai/data_kitchen.py | 74 ++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/freqtrade/freqai/data_kitchen.py b/freqtrade/freqai/data_kitchen.py index 763a07375..62e353949 100644 --- a/freqtrade/freqai/data_kitchen.py +++ b/freqtrade/freqai/data_kitchen.py @@ -723,6 +723,80 @@ class FreqaiDataKitchen: ) return + + def compute_inlier_metric(self) -> None: + """ + + Compute inlier metric from backwards distance distributions. + This metric defines how well features from a timepoint fit + into previous timepoints. + """ + + import scipy.stats as ss + + nmb_previous_points = self.data['InlierMetric_nmb_points'] + weibull_percentile = self.data['InlierMetric_weib_perc'] + + train_ft_df = self.data_dictionary['train_features'] + train_ft_df_reindexed = train_ft_df.reindex( + index=np.flip(train_ft_df.index) + ) + + pairwise = pd.DataFrame( + np.triu( + pairwise_distances(train_ft_df_reindexed, n_jobs=self.thread_count) + ), + columns=train_ft_df_reindexed.index, + index=train_ft_df_reindexed.index + ) + pairwise = pairwise.round(5) + + column_labels = [ + '{}{}'.format('d', i) for i in range(1, nmb_previous_points+1) + ] + distances = pd.DataFrame( + columns=column_labels, index=train_ft_df.index + ) + for index in train_ft_df.index[nmb_previous_points]: + current_row = pairwise.loc[[index]] + current_row_no_zeros = current_row.loc[ + :, (current_row!=0).any(axis=0) + ] + distances.loc[[index]] = current_row_no_zeros.iloc[ + :, :nmb_previous_points + ] + distances = distances.replace([np.inf, -np.inf], np.nan) + drop_index = pd.isnull(distances).any(1) + distances = distances[drop_index==0] + + inliers = pd.DataFrame(index=distances.index) + for key in distances.keys(): + current_distances = distances[key].dropna() + fit_params = ss.weibull_min.fit(current_distances) + cutoff = ss.weibull_min.ppf(weibull_percentile, *fit_params) + is_inlier = np.where( + current_distances<=cutoff, 1, 0 + ) + df_inlier = pd.DataFrame( + {key+'_IsInlier':is_inlier}, index=distances.index + ) + inliers = pd.concat( + [inliers, df_inlier], axis=1 + ) + + self.data_dictionary['train_features'] = pd.DataFrame( + data=inliers.sum(axis=1)/nmb_previous_points, + columns=['inlier_metric'], + index = train_ft_df.index + ) + + percent_outliers = np.round( + 100*(1-self.data_dictionary['iniler_metric'].sum()/ + len(train_ft_df.index)), 2 + ) + logger.info('{percent_outliers}%% of data points were identified as outliers') + + return None def find_features(self, dataframe: DataFrame) -> None: """ From b11742a4c5a7130d7aabc0d20c343372f7911379 Mon Sep 17 00:00:00 2001 From: robcaulk Date: Thu, 18 Aug 2022 19:15:29 +0200 Subject: [PATCH 02/96] integrate inlier metric function --- freqtrade/freqai/data_kitchen.py | 85 ++++++++++++++++++---------- freqtrade/freqai/freqai_interface.py | 36 ++++++++---- 2 files changed, 79 insertions(+), 42 deletions(-) diff --git a/freqtrade/freqai/data_kitchen.py b/freqtrade/freqai/data_kitchen.py index 62e353949..80919626c 100644 --- a/freqtrade/freqai/data_kitchen.py +++ b/freqtrade/freqai/data_kitchen.py @@ -723,81 +723,104 @@ class FreqaiDataKitchen: ) return - - def compute_inlier_metric(self) -> None: + + def compute_inlier_metric(self, set_='train') -> None: """ - - Compute inlier metric from backwards distance distributions. - This metric defines how well features from a timepoint fit + + Compute inlier metric from backwards distance distributions. + This metric defines how well features from a timepoint fit into previous timepoints. """ import scipy.stats as ss - - nmb_previous_points = self.data['InlierMetric_nmb_points'] - weibull_percentile = self.data['InlierMetric_weib_perc'] - train_ft_df = self.data_dictionary['train_features'] - train_ft_df_reindexed = train_ft_df.reindex( - index=np.flip(train_ft_df.index) + no_prev_pts = self.freqai_config["feature_parameters"]["inlier_metric_window"] + weib_pct = self.freqai_config["feature_parameters"]["inlier_metric_weibull_cutoff"] + + if set_ == 'train': + compute_df = copy.deepcopy(self.data_dictionary['train_features']) + elif set_ == 'test': + compute_df = copy.deepcopy(self.data_dictionary['test_features']) + else: + compute_df = copy.deepcopy(self.data_dictionary['prediction_features']) + + compute_df_reindexed = compute_df.reindex( + index=np.flip(compute_df.index) ) pairwise = pd.DataFrame( np.triu( - pairwise_distances(train_ft_df_reindexed, n_jobs=self.thread_count) + pairwise_distances(compute_df_reindexed, n_jobs=self.thread_count) ), - columns=train_ft_df_reindexed.index, - index=train_ft_df_reindexed.index + columns=compute_df_reindexed.index, + index=compute_df_reindexed.index ) pairwise = pairwise.round(5) column_labels = [ - '{}{}'.format('d', i) for i in range(1, nmb_previous_points+1) + '{}{}'.format('d', i) for i in range(1, no_prev_pts + 1) ] distances = pd.DataFrame( - columns=column_labels, index=train_ft_df.index + columns=column_labels, index=compute_df.index ) - for index in train_ft_df.index[nmb_previous_points]: + + for index in compute_df.index[no_prev_pts:]: current_row = pairwise.loc[[index]] current_row_no_zeros = current_row.loc[ - :, (current_row!=0).any(axis=0) + :, (current_row != 0).any(axis=0) ] distances.loc[[index]] = current_row_no_zeros.iloc[ - :, :nmb_previous_points + :, :no_prev_pts ] distances = distances.replace([np.inf, -np.inf], np.nan) drop_index = pd.isnull(distances).any(1) - distances = distances[drop_index==0] + distances = distances[drop_index == 0] inliers = pd.DataFrame(index=distances.index) for key in distances.keys(): current_distances = distances[key].dropna() fit_params = ss.weibull_min.fit(current_distances) - cutoff = ss.weibull_min.ppf(weibull_percentile, *fit_params) + cutoff = ss.weibull_min.ppf(weib_pct, *fit_params) is_inlier = np.where( - current_distances<=cutoff, 1, 0 + current_distances <= cutoff, 1, 0 ) df_inlier = pd.DataFrame( - {key+'_IsInlier':is_inlier}, index=distances.index + {key + '_IsInlier': is_inlier}, index=distances.index ) inliers = pd.concat( [inliers, df_inlier], axis=1 ) - self.data_dictionary['train_features'] = pd.DataFrame( - data=inliers.sum(axis=1)/nmb_previous_points, + inlier_metric = pd.DataFrame( + data=inliers.sum(axis=1) / no_prev_pts, columns=['inlier_metric'], - index = train_ft_df.index + index=compute_df.index ) - percent_outliers = np.round( - 100*(1-self.data_dictionary['iniler_metric'].sum()/ - len(train_ft_df.index)), 2 - ) - logger.info('{percent_outliers}%% of data points were identified as outliers') + inlier_metric = 2 * (inlier_metric - inlier_metric.min()) / \ + (inlier_metric.max() - inlier_metric.min()) - 1 + + if set_ in ('train', 'test'): + inlier_metric = inlier_metric.iloc[no_prev_pts:] + compute_df = compute_df.iloc[no_prev_pts:] + self.remove_beginning_points_from_data_dict(set_, no_prev_pts) + self.data_dictionary[f'{set_}_features'] = pd.concat( + [compute_df, inlier_metric], axis=1) + else: + self.data_dictionary['prediction_features'] = pd.concat( + [compute_df, inlier_metric], axis=1) + self.data_dictionary['prediction_features'].fillna(0, inplace=True) return None + def remove_beginning_points_from_data_dict(self, set_='train', no_prev_pts: int = 10): + features = self.data_dictionary[f'{set_}_features'] + weights = self.data_dictionary[f'{set_}_weights'] + labels = self.data_dictionary[f'{set_}_labels'] + self.data_dictionary[f'{set_}_weights'] = weights[no_prev_pts:] + self.data_dictionary[f'{set_}_features'] = features.iloc[no_prev_pts:] + self.data_dictionary[f'{set_}_labels'] = labels.iloc[no_prev_pts:] + def find_features(self, dataframe: DataFrame) -> None: """ Find features in the strategy provided dataframe diff --git a/freqtrade/freqai/freqai_interface.py b/freqtrade/freqai/freqai_interface.py index 4106f24e0..e6e019b66 100644 --- a/freqtrade/freqai/freqai_interface.py +++ b/freqtrade/freqai/freqai_interface.py @@ -66,7 +66,6 @@ class IFreqaiModel(ABC): "data_split_parameters", {}) self.model_training_parameters: Dict[str, Any] = config.get("freqai", {}).get( "model_training_parameters", {}) - self.feature_parameters = config.get("freqai", {}).get("feature_parameters") self.retrain = False self.first = True self.set_full_path() @@ -74,11 +73,14 @@ class IFreqaiModel(ABC): self.dd = FreqaiDataDrawer(Path(self.full_path), self.config, self.follow_mode) self.identifier: str = self.freqai_info.get("identifier", "no_id_provided") self.scanning = False + self.ft_params = self.freqai_info["feature_parameters"] self.keras: bool = self.freqai_info.get("keras", False) - if self.keras and self.freqai_info.get("feature_parameters", {}).get("DI_threshold", 0): - self.freqai_info["feature_parameters"]["DI_threshold"] = 0 + if self.keras and self.ft_params.get("DI_threshold", 0): + self.ft_params["DI_threshold"] = 0 logger.warning("DI threshold is not configured for Keras models yet. Deactivating.") self.CONV_WIDTH = self.freqai_info.get("conv_width", 2) + if self.ft_params.get("inlier_metric_window", 0): + self.CONV_WIDTH = self.ft_params.get("inlier_metric_window", 0) * 2 self.pair_it = 0 self.pair_it_train = 0 self.total_pairs = len(self.config.get("exchange", {}).get("pair_whitelist")) @@ -403,18 +405,20 @@ class IFreqaiModel(ABC): example of how outlier data points are dropped from the dataframe used for training. """ - if self.freqai_info["feature_parameters"].get( + ft_params = self.freqai_info["feature_parameters"] + + if ft_params.get( "principal_component_analysis", False ): dk.principal_component_analysis() - if self.freqai_info["feature_parameters"].get("use_SVM_to_remove_outliers", False): + if ft_params.get("use_SVM_to_remove_outliers", False): dk.use_SVM_to_remove_outliers(predict=False) - if self.freqai_info["feature_parameters"].get("DI_threshold", 0): + if ft_params.get("DI_threshold", 0): dk.data["avg_mean_dist"] = dk.compute_distances() - if self.freqai_info["feature_parameters"].get("use_DBSCAN_to_remove_outliers", False): + if ft_params.get("use_DBSCAN_to_remove_outliers", False): if dk.pair in self.dd.old_DBSCAN_eps: eps = self.dd.old_DBSCAN_eps[dk.pair] else: @@ -422,6 +426,11 @@ class IFreqaiModel(ABC): dk.use_DBSCAN_to_remove_outliers(predict=False, eps=eps) self.dd.old_DBSCAN_eps[dk.pair] = dk.data['DBSCAN_eps'] + if ft_params.get('inlier_metric_window', 0): + dk.compute_inlier_metric(set_='train') + if self.freqai_info["data_split_parameters"]["test_size"] > 0: + dk.compute_inlier_metric(set_='test') + def data_cleaning_predict(self, dk: FreqaiDataKitchen, dataframe: DataFrame) -> None: """ Base data cleaning method for predict. @@ -433,18 +442,23 @@ class IFreqaiModel(ABC): of how the do_predict vector is modified. do_predict is ultimately passed back to strategy for buy signals. """ - if self.freqai_info["feature_parameters"].get( + ft_params = self.freqai_info["feature_parameters"] + + if ft_params.get('inlier_metric_window', 0): + dk.compute_inlier_metric(set_='predict') + + if ft_params.get( "principal_component_analysis", False ): dk.pca_transform(dataframe) - if self.freqai_info["feature_parameters"].get("use_SVM_to_remove_outliers", False): + if ft_params.get("use_SVM_to_remove_outliers", False): dk.use_SVM_to_remove_outliers(predict=True) - if self.freqai_info["feature_parameters"].get("DI_threshold", 0): + if ft_params.get("DI_threshold", 0): dk.check_if_pred_in_training_spaces() - if self.freqai_info["feature_parameters"].get("use_DBSCAN_to_remove_outliers", False): + if ft_params.get("use_DBSCAN_to_remove_outliers", False): dk.use_DBSCAN_to_remove_outliers(predict=True) def model_exists( From a58dd0bbf9347a69fecde43e5735bc1851de3b9b Mon Sep 17 00:00:00 2001 From: robcaulk Date: Fri, 19 Aug 2022 18:35:24 +0200 Subject: [PATCH 03/96] add noise feature, improve docstrings --- freqtrade/freqai/data_kitchen.py | 11 +++++++++++ freqtrade/freqai/freqai_interface.py | 18 +++++++----------- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/freqtrade/freqai/data_kitchen.py b/freqtrade/freqai/data_kitchen.py index 80919626c..0158996c7 100644 --- a/freqtrade/freqai/data_kitchen.py +++ b/freqtrade/freqai/data_kitchen.py @@ -821,6 +821,17 @@ class FreqaiDataKitchen: self.data_dictionary[f'{set_}_features'] = features.iloc[no_prev_pts:] self.data_dictionary[f'{set_}_labels'] = labels.iloc[no_prev_pts:] + def add_noise_to_training_features(self) -> None: + """ + Add noise to train features to reduce the risk of overfitting. + """ + mu = 0 # no shift + sigma = self.freqai_config["feature_parameters"]["noise_standard_deviation"] + compute_df = self.data_dictionary['train_features'] + noise = np.random.normal(mu, sigma, [compute_df.shape[0], compute_df.shape[1]]) + self.data_dictionary['train_features'] += noise + return + def find_features(self, dataframe: DataFrame) -> None: """ Find features in the strategy provided dataframe diff --git a/freqtrade/freqai/freqai_interface.py b/freqtrade/freqai/freqai_interface.py index e6e019b66..239cb1869 100644 --- a/freqtrade/freqai/freqai_interface.py +++ b/freqtrade/freqai/freqai_interface.py @@ -399,10 +399,9 @@ class IFreqaiModel(ABC): def data_cleaning_train(self, dk: FreqaiDataKitchen) -> None: """ - Base data cleaning method for train - Any function inside this method should drop training data points from the filtered_dataframe - based on user decided logic. See FreqaiDataKitchen::use_SVM_to_remove_outliers() for an - example of how outlier data points are dropped from the dataframe used for training. + Base data cleaning method for train. + Functions here improve/modify the input data by identifying outliers, + computing additional metrics, adding noise, reducing dimensionality etc. """ ft_params = self.freqai_info["feature_parameters"] @@ -431,16 +430,13 @@ class IFreqaiModel(ABC): if self.freqai_info["data_split_parameters"]["test_size"] > 0: dk.compute_inlier_metric(set_='test') + if self.freqai_info["feature_parameters"].get('noise_standard_deviation', 0): + dk.add_noise_to_training_features() + def data_cleaning_predict(self, dk: FreqaiDataKitchen, dataframe: DataFrame) -> None: """ Base data cleaning method for predict. - These functions each modify dk.do_predict, which is a dataframe with equal length - to the number of candles coming from and returning to the strategy. Inside do_predict, - 1 allows prediction and < 0 signals to the strategy that the model is not confident in - the prediction. - See FreqaiDataKitchen::remove_outliers() for an example - of how the do_predict vector is modified. do_predict is ultimately passed back to strategy - for buy signals. + Functions here are complementary to the functions of data_cleaning_train. """ ft_params = self.freqai_info["feature_parameters"] From 7f52908e87a61ed60c7fe3f2b9b933b6211c60da Mon Sep 17 00:00:00 2001 From: robcaulk Date: Tue, 30 Aug 2022 18:55:58 +0200 Subject: [PATCH 04/96] ensure the lost points are prepended for FreqUI --- freqtrade/freqai/freqai_interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqai/freqai_interface.py b/freqtrade/freqai/freqai_interface.py index 239cb1869..893f960ea 100644 --- a/freqtrade/freqai/freqai_interface.py +++ b/freqtrade/freqai/freqai_interface.py @@ -587,7 +587,7 @@ class IFreqaiModel(ABC): # # for keras type models, the conv_window needs to be prepended so # # viewing is correct in frequi - if self.freqai_info.get('keras', False): + if self.freqai_info.get('keras', False) or self.ft_params.get('inlier_metric_window', 0): n_lost_points = self.freqai_info.get('conv_width', 2) zeros_df = DataFrame(np.zeros((n_lost_points, len(hist_preds_df.columns))), columns=hist_preds_df.columns) From 0b8482360f0ec1aea6e61ac08b3746584c03800a Mon Sep 17 00:00:00 2001 From: robcaulk Date: Tue, 30 Aug 2022 20:32:49 +0200 Subject: [PATCH 05/96] add documentation for inlier metric --- docs/freqai.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/freqai.md b/docs/freqai.md index e4a451324..3fc76e9f0 100644 --- a/docs/freqai.md +++ b/docs/freqai.md @@ -115,6 +115,8 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `use_DBSCAN_to_remove_outliers` | Cluster data using DBSCAN to identify and remove outliers from training and prediction data. See details about how it works [here](#removing-outliers-with-dbscan).
**Datatype:** Boolean. | `outlier_protection_percentage` | If more than `outlier_protection_percentage` fraction of points are removed as outliers, FreqAI will log a warning message and ignore outlier detection while keeping the original dataset intact.
**Datatype:** float. Default: `30` | `reverse_train_test_order` | If true, FreqAI will train on the latest data split and test on historical split of the data. This allows the model to be trained up to the most recent data point, while avoiding overfitting. However, users should be careful to understand unorthodox nature of this parameter before employing it.
**Datatype:** bool. Default: False +| `inlier_metric_window` | If set, FreqAI will add the `inlier_metric` to the training feature set and set the lookback to be the `inlier_metric_window`. Details of how the `inlier_metric` is computed can be found [here](#using-the-inliermetric)
**Datatype:** int. Default: 0 +| `inlier_metric_weibull_cutoff` | If the `inlier_metric_window` is set, this value is used to determine the tail cutoff in the weibull distribution fit. Details of how the `inlier_metric` is computed can be found [here](#using-the-inliermetric)
**Datatype:** float. Default: 0.95 | | **Data split parameters** | `data_split_parameters` | Include any additional parameters available from Scikit-learn `test_train_split()`, which are shown [here](https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.train_test_split.html) (external website).
**Datatype:** Dictionary. | `test_size` | Fraction of data that should be used for testing instead of training.
**Datatype:** Positive float < 1. @@ -636,6 +638,20 @@ testing; the other points are used for training. The test data is used to evaluate the performance of the model after training. If the test score is high, the model is able to capture the behavior of the data well. If the test score is low, either the model either does not capture the complexity of the data, the test data is significantly different from the train data, or a different model should be used. +### Using the `inlier_metric` + +The `inlier_metric` is a metric aimed at quantifying how different a prediction data point is from the most recent historic data points. + +User can set `inlier_metric_window` to set the look back window. FreqAI will compute the distance between the present prediction point and each of the previous data points (total of `inlier_metric_window` points). + +This function goes one step further - during training, it computes the `inlier_metric` for all training data points and builds weibull distributions for each each lookback point. If one of the distances falls in the tail of the respective weibull distribution, it is considered an "outlier." If the distance to the lookback point is not in the tail, it is considered an "inlier." Inliers receive a value of 1, and outliers receive a value of 0. + +FreqAI adds this `inlier_metric` score to the training features! Thus, your model is trained to recognize how this temporal inlier metric is evolving. + +Users can control the weibull threshold using the `inlier_metric_weibull_cutoff` + +This function does not currently remove outliers from the data set. + ### Controlling the model learning process Model training parameters are unique to the machine learning library selected by the user. FreqAI allows the user to set any parameter for any library using the `model_training_parameters` dictionary in the user configuration file. The example configuration file (found in `config_examples/config_freqai.example.json`) show some of the example parameters associated with `Catboost` and `LightGBM`, but the user can add any parameters available in those libraries. From 7e8e29e42d4c2b0eb058d9408c27ebd220eb9f68 Mon Sep 17 00:00:00 2001 From: robcaulk Date: Tue, 30 Aug 2022 20:41:37 +0200 Subject: [PATCH 06/96] use continuous value for inlier_metric --- docs/freqai.md | 9 +++------ freqtrade/freqai/data_kitchen.py | 9 +++------ 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/docs/freqai.md b/docs/freqai.md index 3fc76e9f0..2c6efa3b9 100644 --- a/docs/freqai.md +++ b/docs/freqai.md @@ -116,7 +116,6 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `outlier_protection_percentage` | If more than `outlier_protection_percentage` fraction of points are removed as outliers, FreqAI will log a warning message and ignore outlier detection while keeping the original dataset intact.
**Datatype:** float. Default: `30` | `reverse_train_test_order` | If true, FreqAI will train on the latest data split and test on historical split of the data. This allows the model to be trained up to the most recent data point, while avoiding overfitting. However, users should be careful to understand unorthodox nature of this parameter before employing it.
**Datatype:** bool. Default: False | `inlier_metric_window` | If set, FreqAI will add the `inlier_metric` to the training feature set and set the lookback to be the `inlier_metric_window`. Details of how the `inlier_metric` is computed can be found [here](#using-the-inliermetric)
**Datatype:** int. Default: 0 -| `inlier_metric_weibull_cutoff` | If the `inlier_metric_window` is set, this value is used to determine the tail cutoff in the weibull distribution fit. Details of how the `inlier_metric` is computed can be found [here](#using-the-inliermetric)
**Datatype:** float. Default: 0.95 | | **Data split parameters** | `data_split_parameters` | Include any additional parameters available from Scikit-learn `test_train_split()`, which are shown [here](https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.train_test_split.html) (external website).
**Datatype:** Dictionary. | `test_size` | Fraction of data that should be used for testing instead of training.
**Datatype:** Positive float < 1. @@ -644,13 +643,11 @@ The `inlier_metric` is a metric aimed at quantifying how different a prediction User can set `inlier_metric_window` to set the look back window. FreqAI will compute the distance between the present prediction point and each of the previous data points (total of `inlier_metric_window` points). -This function goes one step further - during training, it computes the `inlier_metric` for all training data points and builds weibull distributions for each each lookback point. If one of the distances falls in the tail of the respective weibull distribution, it is considered an "outlier." If the distance to the lookback point is not in the tail, it is considered an "inlier." Inliers receive a value of 1, and outliers receive a value of 0. +This function goes one step further - during training, it computes the `inlier_metric` for all training data points and builds weibull distributions for each each lookback point. The cumulative distribution function for the weibull distribution is used to produce a quantile for each of the data points. The quantiles for each lookback point are averaged to create the `inlier_metric`. -FreqAI adds this `inlier_metric` score to the training features! Thus, your model is trained to recognize how this temporal inlier metric is evolving. +FreqAI adds this `inlier_metric` score to the training features! In other words, your model is trained to recognize how this temporal inlier metric is related to the user set labels. -Users can control the weibull threshold using the `inlier_metric_weibull_cutoff` - -This function does not currently remove outliers from the data set. +This function does **not** remove outliers from the data set. ### Controlling the model learning process diff --git a/freqtrade/freqai/data_kitchen.py b/freqtrade/freqai/data_kitchen.py index 0158996c7..9d4a69287 100644 --- a/freqtrade/freqai/data_kitchen.py +++ b/freqtrade/freqai/data_kitchen.py @@ -735,7 +735,6 @@ class FreqaiDataKitchen: import scipy.stats as ss no_prev_pts = self.freqai_config["feature_parameters"]["inlier_metric_window"] - weib_pct = self.freqai_config["feature_parameters"]["inlier_metric_weibull_cutoff"] if set_ == 'train': compute_df = copy.deepcopy(self.data_dictionary['train_features']) @@ -780,12 +779,10 @@ class FreqaiDataKitchen: for key in distances.keys(): current_distances = distances[key].dropna() fit_params = ss.weibull_min.fit(current_distances) - cutoff = ss.weibull_min.ppf(weib_pct, *fit_params) - is_inlier = np.where( - current_distances <= cutoff, 1, 0 - ) + quantiles = ss.weibull_min.cdf(current_distances, *fit_params) + df_inlier = pd.DataFrame( - {key + '_IsInlier': is_inlier}, index=distances.index + {key: quantiles}, index=distances.index ) inliers = pd.concat( [inliers, df_inlier], axis=1 From df50b1928d6d58b130d1d2b40dcaad334800bcc3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 2 Sep 2022 06:51:30 +0200 Subject: [PATCH 07/96] Fix funding fee calculation for backtesting --- freqtrade/optimize/backtesting.py | 2 +- freqtrade/persistence/trade_model.py | 34 +++++++++++++++++++++++++--- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 77bf3d8ad..8f6b6b332 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -686,7 +686,7 @@ class Backtesting: self.futures_data[trade.pair], amount=trade.amount, is_short=trade.is_short, - open_date=trade.open_date_utc, + open_date=trade.date_last_filled_utc, close_date=exit_candle_time, ) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 1f14f110e..9c9c7f381 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -65,6 +65,8 @@ class Order(_DECL_BASE): order_filled_date = Column(DateTime, nullable=True) order_update_date = Column(DateTime, nullable=True) + funding_fee = Column(Float, nullable=True) + ft_fee_base = Column(Float, nullable=True) @property @@ -72,6 +74,13 @@ class Order(_DECL_BASE): """ Order-date with UTC timezoneinfo""" return self.order_date.replace(tzinfo=timezone.utc) + @property + def order_filled_utc(self) -> Optional[datetime]: + """ last order-date with UTC timezoneinfo""" + return ( + self.order_filled_date.replace(tzinfo=timezone.utc) if self.order_filled_date else None + ) + @property def safe_price(self) -> float: return self.average or self.price @@ -179,6 +188,10 @@ class Order(_DECL_BASE): self.remaining = 0 self.status = 'closed' self.ft_is_open = False + # Assign funding fees to Order. + # Assumes backtesting will use date_last_filled_utc to calculate future funding fees. + self.funding_fee = trade.funding_fees + if (self.ft_order_side == trade.entry_side): trade.open_rate = self.price trade.recalc_trade_from_orders() @@ -346,6 +359,12 @@ class LocalTrade(): else: return self.amount + @property + def date_last_filled_utc(self): + """ Date of the last filled order""" + return max([self.open_date_utc, + max(o.order_filled_utc for o in self.orders if o.filled and not o.ft_is_open)]) + @property def open_date_utc(self): return self.open_date.replace(tzinfo=timezone.utc) @@ -843,10 +862,14 @@ class LocalTrade(): close_profit = 0.0 close_profit_abs = 0.0 profit = None - for o in self.orders: + # Reset funding fees + self.funding_fees = 0.0 + funding_fees = 0.0 + ordercount = len(self.orders) - 1 + for i, o in enumerate(self.orders): if o.ft_is_open or not o.filled: continue - + funding_fees += (o.funding_fee or 0.0) tmp_amount = FtPrecise(o.safe_amount_after_fee) tmp_price = FtPrecise(o.safe_price) @@ -861,7 +884,11 @@ class LocalTrade(): avg_price = current_stake / current_amount if is_exit: - # Process partial exits + # Process exits + if i == ordercount and is_closing: + # Apply funding fees only to the last order + self.funding_fees = funding_fees + exit_rate = o.safe_price exit_amount = o.safe_amount_after_fee profit = self.calc_profit(rate=exit_rate, amount=exit_amount, @@ -871,6 +898,7 @@ class LocalTrade(): exit_rate, amount=exit_amount, open_rate=avg_price) else: total_stake = total_stake + self._calc_open_trade_value(tmp_amount, price) + self.funding_fees = funding_fees if close_profit: self.close_profit = close_profit From 0c6a02687a49b48c433ec841200911b21efc8f24 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 2 Sep 2022 20:55:13 +0200 Subject: [PATCH 08/96] Don't calculate funding fees if we're not going to use them. --- freqtrade/freqtradebot.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 37bc6dfed..95b42155f 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -726,10 +726,11 @@ class FreqtradeBot(LoggingMixin): fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker') base_currency = self.exchange.get_pair_base_currency(pair) open_date = datetime.now(timezone.utc) - funding_fees = self.exchange.get_funding_fees( - pair=pair, amount=amount, is_short=is_short, open_date=open_date) + # This is a new trade if trade is None: + funding_fees = self.exchange.get_funding_fees( + pair=pair, amount=amount, is_short=is_short, open_date=open_date) trade = Trade( pair=pair, base_currency=base_currency, From 0f483ee31f043ceafaa7a5c19eaa77ade2e1d323 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 3 Sep 2022 10:06:23 +0200 Subject: [PATCH 09/96] Use "since last order" approach for live as well. --- freqtrade/freqtradebot.py | 4 ++-- freqtrade/persistence/trade_model.py | 12 +++++++++--- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 95b42155f..5323a5fc0 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -281,7 +281,7 @@ class FreqtradeBot(LoggingMixin): pair=trade.pair, amount=trade.amount, is_short=trade.is_short, - open_date=trade.open_date_utc + open_date=trade.date_last_filled_utc ) trade.funding_fees = funding_fees else: @@ -1485,7 +1485,7 @@ class FreqtradeBot(LoggingMixin): pair=trade.pair, amount=trade.amount, is_short=trade.is_short, - open_date=trade.open_date_utc, + open_date=trade.date_last_filled_utc, ) exit_type = 'exit' exit_reason = exit_tag or exit_check.exit_reason diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 9c9c7f381..c2a87daaa 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -128,6 +128,10 @@ class Order(_DECL_BASE): self.ft_is_open = True if self.status in NON_OPEN_EXCHANGE_STATES: self.ft_is_open = False + if self.trade: + # Assign funding fee up to this point + # (represents the funding fee since the last order) + self.funding_fee = self.trade.funding_fees if (order.get('filled', 0.0) or 0.0) > 0: self.order_filled_date = datetime.now(timezone.utc) self.order_update_date = datetime.now(timezone.utc) @@ -360,10 +364,12 @@ class LocalTrade(): return self.amount @property - def date_last_filled_utc(self): + def date_last_filled_utc(self) -> datetime: """ Date of the last filled order""" - return max([self.open_date_utc, - max(o.order_filled_utc for o in self.orders if o.filled and not o.ft_is_open)]) + orders = self.select_filled_orders() + if not orders: + return self.open_date_utc + return max([self.open_date_utc, max(o.order_filled_utc for o in orders)]) @property def open_date_utc(self): From b95b3d8391f4df3bb3510173711349d7356a08ec Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 3 Sep 2022 15:09:50 +0200 Subject: [PATCH 10/96] Update test to actually test funding fee appliance --- tests/test_persistence.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 23ccc67f3..cdca3bc4d 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -615,21 +615,25 @@ def test_calc_open_close_trade_price( is_short=is_short, leverage=lev, trading_mode=trading_mode, - funding_fees=funding_fees ) entry_order = limit_order[trade.entry_side] exit_order = limit_order[trade.exit_side] trade.open_order_id = f'something-{is_short}-{lev}-{exchange}' oobj = Order.parse_from_ccxt_object(entry_order, 'ADA/USDT', trade.entry_side) - trade.orders.append(oobj) + oobj.trade = trade + oobj.update_from_ccxt_object(entry_order) trade.update_trade(oobj) + trade.funding_fees = funding_fees + oobj = Order.parse_from_ccxt_object(exit_order, 'ADA/USDT', trade.exit_side) - trade.orders.append(oobj) + oobj.trade = trade + oobj.update_from_ccxt_object(exit_order) trade.update_trade(oobj) assert trade.is_open is False + assert trade.funding_fees == funding_fees assert pytest.approx(trade._calc_open_trade_value(trade.amount, trade.open_rate)) == open_value assert pytest.approx(trade.calc_close_trade_value(trade.close_rate)) == close_value From ed4cc18cddaaff7bdb95738ee4a3d5093530b0e0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 3 Sep 2022 15:18:09 +0200 Subject: [PATCH 11/96] Migration to check order funding fee --- freqtrade/persistence/migrations.py | 10 ++++++---- freqtrade/persistence/trade_model.py | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py index 1131c88b4..1f1330ba7 100644 --- a/freqtrade/persistence/migrations.py +++ b/freqtrade/persistence/migrations.py @@ -212,17 +212,18 @@ def migrate_orders_table(engine, table_back_name: str, cols_order: List): ft_fee_base = get_column_def(cols_order, 'ft_fee_base', 'null') average = get_column_def(cols_order, 'average', 'null') stop_price = get_column_def(cols_order, 'stop_price', 'null') + funding_fee = get_column_def(cols_order, 'funding_fee', '0.0') # sqlite does not support literals for booleans with engine.begin() as connection: connection.execute(text(f""" insert into orders (id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id, status, symbol, order_type, side, price, amount, filled, average, remaining, cost, - stop_price, order_date, order_filled_date, order_update_date, ft_fee_base) + stop_price, order_date, order_filled_date, order_update_date, ft_fee_base, funding_fee) select id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id, status, symbol, order_type, side, price, amount, filled, {average} average, remaining, cost, {stop_price} stop_price, order_date, order_filled_date, - order_update_date, {ft_fee_base} ft_fee_base + order_update_date, {ft_fee_base} ft_fee_base {funding_fee} funding_fee from {table_back_name} """)) @@ -307,9 +308,10 @@ def check_migrate(engine, decl_base, previous_tables) -> None: # Check if migration necessary # Migrates both trades and orders table! # if ('orders' not in previous_tables - # or not has_column(cols_orders, 'stop_price')): + # or not has_column(cols_orders, 'funding_fee')): migrating = False - if not has_column(cols_trades, 'contract_size'): + if not has_column(cols_orders, 'funding_fee'): + # if not has_column(cols_trades, 'contract_size'): migrating = True logger.info(f"Running database migration for trades - " f"backup: {table_back_name}, {order_table_bak_name}") diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index c2a87daaa..6127c502d 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -892,7 +892,7 @@ class LocalTrade(): if is_exit: # Process exits if i == ordercount and is_closing: - # Apply funding fees only to the last order + # Apply funding fees only to the last closing order self.funding_fees = funding_fees exit_rate = o.safe_price From c9be66b5b639f151abb1d9a5e76267d752eddb3b Mon Sep 17 00:00:00 2001 From: robcaulk Date: Sat, 3 Sep 2022 15:52:29 +0200 Subject: [PATCH 12/96] increase test coverage for dk, improve function naming, extra cleaning --- freqtrade/freqai/data_kitchen.py | 30 ++++++----- freqtrade/freqai/freqai_interface.py | 10 ++-- tests/freqai/conftest.py | 32 +++++++++++ tests/freqai/test_freqai_datakitchen.py | 72 ++++++++++++++++++++++++- tests/freqai/test_freqai_interface.py | 34 ++++++------ 5 files changed, 143 insertions(+), 35 deletions(-) diff --git a/freqtrade/freqai/data_kitchen.py b/freqtrade/freqai/data_kitchen.py index 9d4a69287..fce9e8480 100644 --- a/freqtrade/freqai/data_kitchen.py +++ b/freqtrade/freqai/data_kitchen.py @@ -1,7 +1,7 @@ import copy -import datetime import logging import shutil +from datetime import datetime, timezone from pathlib import Path from typing import Any, Dict, List, Tuple @@ -345,7 +345,7 @@ class FreqaiDataKitchen: def denormalize_labels_from_metadata(self, df: DataFrame) -> DataFrame: """ - Normalize a set of data using the mean and standard deviation from + Denormalize a set of data using the mean and standard deviation from the associated training data. :param df: Dataframe of predictions to be denormalized """ @@ -384,7 +384,7 @@ class FreqaiDataKitchen: config_timerange = TimeRange.parse_timerange(self.config["timerange"]) if config_timerange.stopts == 0: config_timerange.stopts = int( - datetime.datetime.now(tz=datetime.timezone.utc).timestamp() + datetime.now(tz=timezone.utc).timestamp() ) timerange_train = copy.deepcopy(full_timerange) timerange_backtest = copy.deepcopy(full_timerange) @@ -401,8 +401,8 @@ class FreqaiDataKitchen: timerange_train.stopts = timerange_train.startts + train_period_days first = False - start = datetime.datetime.utcfromtimestamp(timerange_train.startts) - stop = datetime.datetime.utcfromtimestamp(timerange_train.stopts) + start = datetime.utcfromtimestamp(timerange_train.startts) + stop = datetime.utcfromtimestamp(timerange_train.stopts) tr_training_list.append(start.strftime("%Y%m%d") + "-" + stop.strftime("%Y%m%d")) tr_training_list_timerange.append(copy.deepcopy(timerange_train)) @@ -415,8 +415,8 @@ class FreqaiDataKitchen: if timerange_backtest.stopts > config_timerange.stopts: timerange_backtest.stopts = config_timerange.stopts - start = datetime.datetime.utcfromtimestamp(timerange_backtest.startts) - stop = datetime.datetime.utcfromtimestamp(timerange_backtest.stopts) + start = datetime.utcfromtimestamp(timerange_backtest.startts) + stop = datetime.utcfromtimestamp(timerange_backtest.stopts) tr_backtesting_list.append(start.strftime("%Y%m%d") + "-" + stop.strftime("%Y%m%d")) tr_backtesting_list_timerange.append(copy.deepcopy(timerange_backtest)) @@ -436,8 +436,8 @@ class FreqaiDataKitchen: it is sliced down to just the present training period. """ - start = datetime.datetime.fromtimestamp(timerange.startts, tz=datetime.timezone.utc) - stop = datetime.datetime.fromtimestamp(timerange.stopts, tz=datetime.timezone.utc) + start = datetime.fromtimestamp(timerange.startts, tz=timezone.utc) + stop = datetime.fromtimestamp(timerange.stopts, tz=timezone.utc) df = df.loc[df["date"] >= start, :] df = df.loc[df["date"] <= stop, :] @@ -808,6 +808,8 @@ class FreqaiDataKitchen: [compute_df, inlier_metric], axis=1) self.data_dictionary['prediction_features'].fillna(0, inplace=True) + logger.info('Inlier metric computed and added to features.') + return None def remove_beginning_points_from_data_dict(self, set_='train', no_prev_pts: int = 10): @@ -948,14 +950,14 @@ class FreqaiDataKitchen: "Please indicate the end date of your desired backtesting. " "timerange.") # backtest_timerange.stopts = int( - # datetime.datetime.now(tz=datetime.timezone.utc).timestamp() + # datetime.now(tz=timezone.utc).timestamp() # ) backtest_timerange.startts = ( backtest_timerange.startts - backtest_period_days * SECONDS_IN_DAY ) - start = datetime.datetime.utcfromtimestamp(backtest_timerange.startts) - stop = datetime.datetime.utcfromtimestamp(backtest_timerange.stopts) + start = datetime.utcfromtimestamp(backtest_timerange.startts) + stop = datetime.utcfromtimestamp(backtest_timerange.stopts) full_timerange = start.strftime("%Y%m%d") + "-" + stop.strftime("%Y%m%d") self.full_path = Path( @@ -981,7 +983,7 @@ class FreqaiDataKitchen: :return: bool = If the model is expired or not. """ - time = datetime.datetime.now(tz=datetime.timezone.utc).timestamp() + time = datetime.now(tz=timezone.utc).timestamp() elapsed_time = (time - trained_timestamp) / 3600 # hours max_time = self.freqai_config.get("expiration_hours", 0) if max_time > 0: @@ -993,7 +995,7 @@ class FreqaiDataKitchen: self, trained_timestamp: int ) -> Tuple[bool, TimeRange, TimeRange]: - time = datetime.datetime.now(tz=datetime.timezone.utc).timestamp() + time = datetime.now(tz=timezone.utc).timestamp() trained_timerange = TimeRange() data_load_timerange = TimeRange() diff --git a/freqtrade/freqai/freqai_interface.py b/freqtrade/freqai/freqai_interface.py index 893f960ea..fd0554248 100644 --- a/freqtrade/freqai/freqai_interface.py +++ b/freqtrade/freqai/freqai_interface.py @@ -1,10 +1,10 @@ # import contextlib -import datetime import logging import shutil import threading import time from abc import ABC, abstractmethod +from datetime import datetime from pathlib import Path from threading import Lock from typing import Any, Dict, Tuple @@ -174,7 +174,7 @@ class IFreqaiModel(ABC): if retrain: self.train_timer('start') - self.train_model_in_series( + self.extract_data_and_train_model( new_trained_timerange, pair, strategy, dk, data_load_timerange ) self.train_timer('stop') @@ -214,10 +214,10 @@ class IFreqaiModel(ABC): dataframe_backtest = dk.slice_dataframe(tr_backtest, dataframe) trained_timestamp = tr_train - tr_train_startts_str = datetime.datetime.utcfromtimestamp(tr_train.startts).strftime( + tr_train_startts_str = datetime.utcfromtimestamp(tr_train.startts).strftime( "%Y-%m-%d %H:%M:%S" ) - tr_train_stopts_str = datetime.datetime.utcfromtimestamp(tr_train.stopts).strftime( + tr_train_stopts_str = datetime.utcfromtimestamp(tr_train.stopts).strftime( "%Y-%m-%d %H:%M:%S" ) logger.info( @@ -495,7 +495,7 @@ class IFreqaiModel(ABC): Path(self.full_path, Path(self.config["config_files"][0]).name), ) - def train_model_in_series( + def extract_data_and_train_model( self, new_trained_timerange: TimeRange, pair: str, diff --git a/tests/freqai/conftest.py b/tests/freqai/conftest.py index dd148da77..6528347e8 100644 --- a/tests/freqai/conftest.py +++ b/tests/freqai/conftest.py @@ -82,6 +82,38 @@ def get_patched_freqaimodel(mocker, freqaiconf): return freqaimodel +def make_unfiltered_dataframe(mocker, freqai_conf): + freqai_conf.update({"timerange": "20180110-20180130"}) + + strategy = get_patched_freqai_strategy(mocker, freqai_conf) + exchange = get_patched_exchange(mocker, freqai_conf) + strategy.dp = DataProvider(freqai_conf, exchange) + strategy.freqai_info = freqai_conf.get("freqai", {}) + freqai = strategy.freqai + freqai.live = True + freqai.dk = FreqaiDataKitchen(freqai_conf) + freqai.dk.pair = "ADA/BTC" + timerange = TimeRange.parse_timerange("20180110-20180130") + freqai.dd.load_all_pair_histories(timerange, freqai.dk) + + freqai.dd.pair_dict = MagicMock() + + data_load_timerange = TimeRange.parse_timerange("20180110-20180130") + new_timerange = TimeRange.parse_timerange("20180120-20180130") + + corr_dataframes, base_dataframes = freqai.dd.get_base_and_corr_dataframes( + data_load_timerange, freqai.dk.pair, freqai.dk + ) + + unfiltered_dataframe = freqai.dk.use_strategy_to_populate_indicators( + strategy, corr_dataframes, base_dataframes, freqai.dk.pair + ) + + unfiltered_dataframe = freqai.dk.slice_dataframe(new_timerange, unfiltered_dataframe) + + return freqai, unfiltered_dataframe + + def make_data_dictionary(mocker, freqai_conf): freqai_conf.update({"timerange": "20180110-20180130"}) diff --git a/tests/freqai/test_freqai_datakitchen.py b/tests/freqai/test_freqai_datakitchen.py index 9ef955695..2204e94c6 100644 --- a/tests/freqai/test_freqai_datakitchen.py +++ b/tests/freqai/test_freqai_datakitchen.py @@ -6,7 +6,8 @@ import pytest from freqtrade.exceptions import OperationalException from tests.conftest import log_has_re -from tests.freqai.conftest import get_patched_data_kitchen, make_data_dictionary +from tests.freqai.conftest import (get_patched_data_kitchen, make_data_dictionary, + make_unfiltered_dataframe) @pytest.mark.parametrize( @@ -94,3 +95,72 @@ def test_use_SVM_to_remove_outliers_and_outlier_protection(mocker, freqai_conf, "SVM detected 8.46%", caplog, ) + + +def test_compute_inlier_metric(mocker, freqai_conf, caplog): + freqai = make_data_dictionary(mocker, freqai_conf) + freqai_conf['freqai']['feature_parameters'].update({"inlier_metric_window": 10}) + freqai.dk.compute_inlier_metric(set_='train') + assert log_has_re( + "Inlier metric computed and added to features.", + caplog, + ) + + +def test_add_noise_to_training_features(mocker, freqai_conf): + freqai = make_data_dictionary(mocker, freqai_conf) + freqai_conf['freqai']['feature_parameters'].update({"noise_standard_deviation": 0.1}) + freqai.dk.add_noise_to_training_features() + + +def test_remove_beginning_points_from_data_dict(mocker, freqai_conf): + freqai = make_data_dictionary(mocker, freqai_conf) + freqai.dk.remove_beginning_points_from_data_dict(set_='train') + + +def test_principal_component_analysis(mocker, freqai_conf, caplog): + freqai = make_data_dictionary(mocker, freqai_conf) + freqai.dk.principal_component_analysis() + assert log_has_re( + "reduced feature dimension by", + caplog, + ) + + +def test_normalize_data(mocker, freqai_conf): + freqai = make_data_dictionary(mocker, freqai_conf) + data_dict = freqai.dk.data_dictionary + freqai.dk.normalize_data(data_dict) + assert len(freqai.dk.data) == 56 + + +def test_filter_features(mocker, freqai_conf): + freqai, unfiltered_dataframe = make_unfiltered_dataframe(mocker, freqai_conf) + freqai.dk.find_features(unfiltered_dataframe) + + filtered_df, labels = freqai.dk.filter_features( + unfiltered_dataframe, + freqai.dk.training_features_list, + freqai.dk.label_list, + training_filter=True, + ) + + assert len(filtered_df.columns) == 26 + + +def test_make_train_test_datasets(mocker, freqai_conf): + freqai, unfiltered_dataframe = make_unfiltered_dataframe(mocker, freqai_conf) + freqai.dk.find_features(unfiltered_dataframe) + + features_filtered, labels_filtered = freqai.dk.filter_features( + unfiltered_dataframe, + freqai.dk.training_features_list, + freqai.dk.label_list, + training_filter=True, + ) + + data_dictionary = freqai.dk.make_train_test_datasets(features_filtered, labels_filtered) + + assert data_dictionary + assert len(data_dictionary) == 7 + assert len(data_dictionary['train_features'].index) == 1916 diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index 792ffc467..927af2a02 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -17,7 +17,7 @@ def is_arm() -> bool: return "arm" in machine or "aarch64" in machine -def test_train_model_in_series_LightGBM(mocker, freqai_conf): +def test_extract_data_and_train_model_LightGBM(mocker, freqai_conf): freqai_conf.update({"timerange": "20180110-20180130"}) strategy = get_patched_freqai_strategy(mocker, freqai_conf) @@ -35,7 +35,8 @@ def test_train_model_in_series_LightGBM(mocker, freqai_conf): data_load_timerange = TimeRange.parse_timerange("20180110-20180130") new_timerange = TimeRange.parse_timerange("20180120-20180130") - freqai.train_model_in_series(new_timerange, "ADA/BTC", strategy, freqai.dk, data_load_timerange) + freqai.extract_data_and_train_model( + new_timerange, "ADA/BTC", strategy, freqai.dk, data_load_timerange) assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_model.joblib").is_file() assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_metadata.json").is_file() @@ -45,7 +46,7 @@ def test_train_model_in_series_LightGBM(mocker, freqai_conf): shutil.rmtree(Path(freqai.dk.full_path)) -def test_train_model_in_series_LightGBMMultiModel(mocker, freqai_conf): +def test_extract_data_and_train_model_LightGBMMultiModel(mocker, freqai_conf): freqai_conf.update({"timerange": "20180110-20180130"}) freqai_conf.update({"strategy": "freqai_test_multimodel_strat"}) freqai_conf.update({"freqaimodel": "LightGBMRegressorMultiTarget"}) @@ -64,7 +65,8 @@ def test_train_model_in_series_LightGBMMultiModel(mocker, freqai_conf): data_load_timerange = TimeRange.parse_timerange("20180110-20180130") new_timerange = TimeRange.parse_timerange("20180120-20180130") - freqai.train_model_in_series(new_timerange, "ADA/BTC", strategy, freqai.dk, data_load_timerange) + freqai.extract_data_and_train_model( + new_timerange, "ADA/BTC", strategy, freqai.dk, data_load_timerange) assert len(freqai.dk.label_list) == 2 assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_model.joblib").is_file() @@ -77,7 +79,7 @@ def test_train_model_in_series_LightGBMMultiModel(mocker, freqai_conf): @pytest.mark.skipif(is_arm(), reason="no ARM for Catboost ...") -def test_train_model_in_series_Catboost(mocker, freqai_conf): +def test_extract_data_and_train_model_Catboost(mocker, freqai_conf): freqai_conf.update({"timerange": "20180110-20180130"}) freqai_conf.update({"freqaimodel": "CatboostRegressor"}) # freqai_conf.get('freqai', {}).update( @@ -98,8 +100,8 @@ def test_train_model_in_series_Catboost(mocker, freqai_conf): data_load_timerange = TimeRange.parse_timerange("20180110-20180130") new_timerange = TimeRange.parse_timerange("20180120-20180130") - freqai.train_model_in_series(new_timerange, "ADA/BTC", - strategy, freqai.dk, data_load_timerange) + freqai.extract_data_and_train_model(new_timerange, "ADA/BTC", + strategy, freqai.dk, data_load_timerange) assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_model.joblib").exists() assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_metadata.json").exists() @@ -110,7 +112,7 @@ def test_train_model_in_series_Catboost(mocker, freqai_conf): @pytest.mark.skipif(is_arm(), reason="no ARM for Catboost ...") -def test_train_model_in_series_CatboostClassifier(mocker, freqai_conf): +def test_extract_data_and_train_model_CatboostClassifier(mocker, freqai_conf): freqai_conf.update({"timerange": "20180110-20180130"}) freqai_conf.update({"freqaimodel": "CatboostClassifier"}) freqai_conf.update({"strategy": "freqai_test_classifier"}) @@ -130,8 +132,8 @@ def test_train_model_in_series_CatboostClassifier(mocker, freqai_conf): data_load_timerange = TimeRange.parse_timerange("20180110-20180130") new_timerange = TimeRange.parse_timerange("20180120-20180130") - freqai.train_model_in_series(new_timerange, "ADA/BTC", - strategy, freqai.dk, data_load_timerange) + freqai.extract_data_and_train_model(new_timerange, "ADA/BTC", + strategy, freqai.dk, data_load_timerange) assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_model.joblib").exists() assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_metadata.json").exists() @@ -141,7 +143,7 @@ def test_train_model_in_series_CatboostClassifier(mocker, freqai_conf): shutil.rmtree(Path(freqai.dk.full_path)) -def test_train_model_in_series_LightGBMClassifier(mocker, freqai_conf): +def test_extract_data_and_train_model_LightGBMClassifier(mocker, freqai_conf): freqai_conf.update({"timerange": "20180110-20180130"}) freqai_conf.update({"freqaimodel": "LightGBMClassifier"}) freqai_conf.update({"strategy": "freqai_test_classifier"}) @@ -161,8 +163,8 @@ def test_train_model_in_series_LightGBMClassifier(mocker, freqai_conf): data_load_timerange = TimeRange.parse_timerange("20180110-20180130") new_timerange = TimeRange.parse_timerange("20180120-20180130") - freqai.train_model_in_series(new_timerange, "ADA/BTC", - strategy, freqai.dk, data_load_timerange) + freqai.extract_data_and_train_model(new_timerange, "ADA/BTC", + strategy, freqai.dk, data_load_timerange) assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_model.joblib").exists() assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_metadata.json").exists() @@ -289,7 +291,8 @@ def test_follow_mode(mocker, freqai_conf): data_load_timerange = TimeRange.parse_timerange("20180110-20180130") new_timerange = TimeRange.parse_timerange("20180120-20180130") - freqai.train_model_in_series(new_timerange, "ADA/BTC", strategy, freqai.dk, data_load_timerange) + freqai.extract_data_and_train_model( + new_timerange, "ADA/BTC", strategy, freqai.dk, data_load_timerange) assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_model.joblib").is_file() assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_metadata.json").is_file() @@ -338,7 +341,8 @@ def test_principal_component_analysis(mocker, freqai_conf): data_load_timerange = TimeRange.parse_timerange("20180110-20180130") new_timerange = TimeRange.parse_timerange("20180120-20180130") - freqai.train_model_in_series(new_timerange, "ADA/BTC", strategy, freqai.dk, data_load_timerange) + freqai.extract_data_and_train_model( + new_timerange, "ADA/BTC", strategy, freqai.dk, data_load_timerange) assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_pca_object.pkl") From fa8d5b9834ab1ba872e5ca2397e24ed3df9968a7 Mon Sep 17 00:00:00 2001 From: robcaulk Date: Sat, 3 Sep 2022 16:05:18 +0200 Subject: [PATCH 13/96] add documentation for noise_standard_deviation` --- docs/freqai.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/freqai.md b/docs/freqai.md index 2c6efa3b9..d504c93d6 100644 --- a/docs/freqai.md +++ b/docs/freqai.md @@ -116,6 +116,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `outlier_protection_percentage` | If more than `outlier_protection_percentage` fraction of points are removed as outliers, FreqAI will log a warning message and ignore outlier detection while keeping the original dataset intact.
**Datatype:** float. Default: `30` | `reverse_train_test_order` | If true, FreqAI will train on the latest data split and test on historical split of the data. This allows the model to be trained up to the most recent data point, while avoiding overfitting. However, users should be careful to understand unorthodox nature of this parameter before employing it.
**Datatype:** bool. Default: False | `inlier_metric_window` | If set, FreqAI will add the `inlier_metric` to the training feature set and set the lookback to be the `inlier_metric_window`. Details of how the `inlier_metric` is computed can be found [here](#using-the-inliermetric)
**Datatype:** int. Default: 0 +| `noise_standard_deviation` | If > 0, FreqAI adds noise to the training features. FreqAI generates random deviates from a gaussian distribution with a standard deviation of `noise_standard_deviation` and adds them to all data points. Value should be kept relative to the normalized space between -1 and 1). In other words, since data is always normalized between -1 and 1 in FreqAI, the user can expect a `noise_standard_deviation: 0.05` to see 32% of data randomly increased/decreased by more than 2.5%. Good for preventing overfitting.
**Datatype:** int. Default: 0 | | **Data split parameters** | `data_split_parameters` | Include any additional parameters available from Scikit-learn `test_train_split()`, which are shown [here](https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.train_test_split.html) (external website).
**Datatype:** Dictionary. | `test_size` | Fraction of data that should be used for testing instead of training.
**Datatype:** Positive float < 1. From 16573b19e3d478b3a5625a81d2504823dee4cdf8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 3 Sep 2022 19:34:38 +0200 Subject: [PATCH 14/96] Fix migration syntax error --- freqtrade/persistence/migrations.py | 4 ++-- freqtrade/persistence/trade_model.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py index 1f1330ba7..a54c5570f 100644 --- a/freqtrade/persistence/migrations.py +++ b/freqtrade/persistence/migrations.py @@ -223,7 +223,7 @@ def migrate_orders_table(engine, table_back_name: str, cols_order: List): select id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id, status, symbol, order_type, side, price, amount, filled, {average} average, remaining, cost, {stop_price} stop_price, order_date, order_filled_date, - order_update_date, {ft_fee_base} ft_fee_base {funding_fee} funding_fee + order_update_date, {ft_fee_base} ft_fee_base, {funding_fee} funding_fee from {table_back_name} """)) @@ -310,8 +310,8 @@ def check_migrate(engine, decl_base, previous_tables) -> None: # if ('orders' not in previous_tables # or not has_column(cols_orders, 'funding_fee')): migrating = False - if not has_column(cols_orders, 'funding_fee'): # if not has_column(cols_trades, 'contract_size'): + if not has_column(cols_orders, 'funding_fee'): migrating = True logger.info(f"Running database migration for trades - " f"backup: {table_back_name}, {order_table_bak_name}") diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 6127c502d..ea60796a4 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -369,7 +369,8 @@ class LocalTrade(): orders = self.select_filled_orders() if not orders: return self.open_date_utc - return max([self.open_date_utc, max(o.order_filled_utc for o in orders)]) + return max([self.open_date_utc, + max(o.order_filled_utc for o in orders if o.order_filled_utc)]) @property def open_date_utc(self): From dae3b3d86adde0f6c7065ce1d083d9ceac62e5ef Mon Sep 17 00:00:00 2001 From: Timothy Pogue Date: Sat, 3 Sep 2022 13:24:14 -0600 Subject: [PATCH 15/96] support shutting down freqai --- freqtrade/freqai/freqai_interface.py | 37 +++++++++++++++++++--------- freqtrade/freqtradebot.py | 2 ++ freqtrade/strategy/interface.py | 10 ++++++++ 3 files changed, 38 insertions(+), 11 deletions(-) diff --git a/freqtrade/freqai/freqai_interface.py b/freqtrade/freqai/freqai_interface.py index eac5cba07..a9c21fb65 100644 --- a/freqtrade/freqai/freqai_interface.py +++ b/freqtrade/freqai/freqai_interface.py @@ -7,7 +7,7 @@ import time from abc import ABC, abstractmethod from pathlib import Path from threading import Lock -from typing import Any, Dict, Tuple +from typing import Any, Dict, List, Tuple import numpy as np import pandas as pd @@ -27,13 +27,6 @@ pd.options.mode.chained_assignment = None logger = logging.getLogger(__name__) -def threaded(fn): - def wrapper(*args, **kwargs): - threading.Thread(target=fn, args=args, kwargs=kwargs).start() - - return wrapper - - class IFreqaiModel(ABC): """ Class containing all tools for training and prediction in the strategy. @@ -94,6 +87,9 @@ class IFreqaiModel(ABC): self.begin_time_train: float = 0 self.base_tf_seconds = timeframe_to_seconds(self.config['timeframe']) + self._threads: List[threading.Thread] = [] + self._stop_event = threading.Event() + def assert_config(self, config: Dict[str, Any]) -> None: if not config.get("freqai", {}): @@ -147,15 +143,34 @@ class IFreqaiModel(ABC): self.model = None self.dk = None - @threaded - def start_scanning(self, strategy: IStrategy) -> None: + def shutdown(self): + """ + Cleans up threads on Shutdown, set stop event. Join threads to wait + for current training iteration. + """ + logger.info("Stopping FreqAI") + self._stop_event.set() + + logger.info("Waiting on Training iteration") + for _thread in self._threads: + _thread.join() + + def start_scanning(self, *args, **kwargs) -> None: + """ + Start `self._start_scanning` in a separate thread + """ + _thread = threading.Thread(target=self._start_scanning, args=args, kwargs=kwargs) + self._threads.append(_thread) + _thread.start() + + def _start_scanning(self, strategy: IStrategy) -> None: """ Function designed to constantly scan pairs for retraining on a separate thread (intracandle) to improve model youth. This function is agnostic to data preparation/collection/storage, it simply trains on what ever data is available in the self.dd. :param strategy: IStrategy = The user defined strategy class """ - while 1: + while not self._stop_event.is_set(): time.sleep(1) for pair in self.config.get("exchange", {}).get("pair_whitelist"): diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 37bc6dfed..883417219 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -148,6 +148,8 @@ class FreqtradeBot(LoggingMixin): self.check_for_open_trades() + self.strategy.ft_bot_cleanup() + self.rpc.cleanup() Trade.commit() self.exchange.close() diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index c7ea95bda..562a16b18 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -168,6 +168,10 @@ class IStrategy(ABC, HyperStrategyMixin): raise OperationalException( 'freqAI is not enabled. ' 'Please enable it in your config to use this strategy.') + + def cleanup(self, *args, **kwargs): + pass + self.freqai = DummyClass() # type: ignore def ft_bot_start(self, **kwargs) -> None: @@ -181,6 +185,12 @@ class IStrategy(ABC, HyperStrategyMixin): self.ft_load_hyper_params(self.config.get('runmode') == RunMode.HYPEROPT) + def ft_bot_cleanup(self) -> None: + """ + Clean up FreqAI and child threads + """ + self.freqai.shutdown() + @abstractmethod def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ From 3b5e5fc57b7a11402c76f13bddede071bd9eaf9e Mon Sep 17 00:00:00 2001 From: Timothy Pogue Date: Sat, 3 Sep 2022 14:10:23 -0600 Subject: [PATCH 16/96] fix method name in dummy class --- freqtrade/strategy/interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 562a16b18..70cc7fdb3 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -169,7 +169,7 @@ class IStrategy(ABC, HyperStrategyMixin): 'freqAI is not enabled. ' 'Please enable it in your config to use this strategy.') - def cleanup(self, *args, **kwargs): + def shutdown(self, *args, **kwargs): pass self.freqai = DummyClass() # type: ignore From ec76214d023a6c53ffab0af8d43bc5b72b1d66af Mon Sep 17 00:00:00 2001 From: robcaulk Date: Sun, 4 Sep 2022 15:56:07 +0200 Subject: [PATCH 17/96] backup historical predictions pickle and load the backup in case of corruption --- freqtrade/freqai/data_drawer.py | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/freqtrade/freqai/data_drawer.py b/freqtrade/freqai/data_drawer.py index b6a1a15d7..9eeabef8f 100644 --- a/freqtrade/freqai/data_drawer.py +++ b/freqtrade/freqai/data_drawer.py @@ -76,6 +76,8 @@ class FreqaiDataDrawer: self.full_path / f"follower_dictionary-{self.follower_name}.json" ) self.historic_predictions_path = Path(self.full_path / "historic_predictions.pkl") + self.historic_predictions_bkp_path = Path( + self.full_path / "historic_predictions.backup.pkl") self.pair_dictionary_path = Path(self.full_path / "pair_dictionary.json") self.follow_mode = follow_mode if follow_mode: @@ -118,13 +120,21 @@ class FreqaiDataDrawer: """ exists = self.historic_predictions_path.is_file() if exists: - with open(self.historic_predictions_path, "rb") as fp: - self.historic_predictions = cloudpickle.load(fp) - logger.info( - f"Found existing historic predictions at {self.full_path}, but beware " - "that statistics may be inaccurate if the bot has been offline for " - "an extended period of time." - ) + try: + with open(self.historic_predictions_path, "rb") as fp: + self.historic_predictions = cloudpickle.load(fp) + logger.info( + f"Found existing historic predictions at {self.full_path}, but beware " + "that statistics may be inaccurate if the bot has been offline for " + "an extended period of time." + ) + except EOFError: + logger.warning( + 'Historical prediction file was corrupted. Trying to load backup file.') + with open(self.historic_predictions_bkp_path, "rb") as fp: + self.historic_predictions = cloudpickle.load(fp) + logger.warning('FreqAI successfully loaded the backup historical predictions file.') + elif not self.follow_mode: logger.info("Could not find existing historic_predictions, starting from scratch") else: @@ -142,6 +152,9 @@ class FreqaiDataDrawer: with open(self.historic_predictions_path, "wb") as fp: cloudpickle.dump(self.historic_predictions, fp, protocol=cloudpickle.DEFAULT_PROTOCOL) + # create a backup + shutil.copy(self.historic_predictions_path, self.historic_predictions_bkp_path) + def save_drawer_to_disk(self): """ Save data drawer full of all pair model metadata in present model folder. From d43ed186fcc375f9087b059a00d2fb3b5ec6fdfe Mon Sep 17 00:00:00 2001 From: Wagner Costa Santos Date: Sun, 4 Sep 2022 16:55:47 -0300 Subject: [PATCH 18/96] fix issue with freqai backtesting at slice dataframe --- freqtrade/freqai/data_kitchen.py | 15 +++++++++++++++ freqtrade/freqai/freqai_interface.py | 4 ++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqai/data_kitchen.py b/freqtrade/freqai/data_kitchen.py index 7595942fe..d525ae4ac 100644 --- a/freqtrade/freqai/data_kitchen.py +++ b/freqtrade/freqai/data_kitchen.py @@ -458,6 +458,21 @@ class FreqaiDataKitchen: return df + def slice_dataframe_backtesting(self, timerange: TimeRange, df: DataFrame) -> DataFrame: + """ + Given a full dataframe, extract the user desired window + :param tr: timerange string that we wish to extract from df + :param df: Dataframe containing all candles to run the entire backtest. Here + it is sliced down to just the present training period. + """ + + start = datetime.datetime.fromtimestamp(timerange.startts, tz=datetime.timezone.utc) + stop = datetime.datetime.fromtimestamp(timerange.stopts, tz=datetime.timezone.utc) + df = df.loc[df["date"] >= start, :] + df = df.loc[df["date"] < stop, :] + + return df + def principal_component_analysis(self) -> None: """ Performs Principal Component Analysis on the data for dimensionality reduction diff --git a/freqtrade/freqai/freqai_interface.py b/freqtrade/freqai/freqai_interface.py index a9c21fb65..b17f8c9ab 100644 --- a/freqtrade/freqai/freqai_interface.py +++ b/freqtrade/freqai/freqai_interface.py @@ -225,8 +225,8 @@ class IFreqaiModel(ABC): train_it += 1 total_trains = len(dk.backtesting_timeranges) self.training_timerange = tr_train - dataframe_train = dk.slice_dataframe(tr_train, dataframe) - dataframe_backtest = dk.slice_dataframe(tr_backtest, dataframe) + dataframe_train = dk.slice_dataframe_backtesting(tr_train, dataframe) + dataframe_backtest = dk.slice_dataframe_backtesting(tr_backtest, dataframe) trained_timestamp = tr_train tr_train_startts_str = datetime.datetime.utcfromtimestamp(tr_train.startts).strftime( From 78d01810ed1dd3ecee954b914e9066267d5e969a Mon Sep 17 00:00:00 2001 From: robcaulk Date: Mon, 5 Sep 2022 00:07:05 +0200 Subject: [PATCH 19/96] reduce code redundancy, ensure live always gets the latest data --- freqtrade/freqai/data_kitchen.py | 18 ++---------------- freqtrade/freqai/freqai_interface.py | 4 ++-- 2 files changed, 4 insertions(+), 18 deletions(-) diff --git a/freqtrade/freqai/data_kitchen.py b/freqtrade/freqai/data_kitchen.py index d525ae4ac..828734520 100644 --- a/freqtrade/freqai/data_kitchen.py +++ b/freqtrade/freqai/data_kitchen.py @@ -454,22 +454,8 @@ class FreqaiDataKitchen: start = datetime.datetime.fromtimestamp(timerange.startts, tz=datetime.timezone.utc) stop = datetime.datetime.fromtimestamp(timerange.stopts, tz=datetime.timezone.utc) df = df.loc[df["date"] >= start, :] - df = df.loc[df["date"] <= stop, :] - - return df - - def slice_dataframe_backtesting(self, timerange: TimeRange, df: DataFrame) -> DataFrame: - """ - Given a full dataframe, extract the user desired window - :param tr: timerange string that we wish to extract from df - :param df: Dataframe containing all candles to run the entire backtest. Here - it is sliced down to just the present training period. - """ - - start = datetime.datetime.fromtimestamp(timerange.startts, tz=datetime.timezone.utc) - stop = datetime.datetime.fromtimestamp(timerange.stopts, tz=datetime.timezone.utc) - df = df.loc[df["date"] >= start, :] - df = df.loc[df["date"] < stop, :] + if not self.live: + df = df.loc[df["date"] < stop, :] return df diff --git a/freqtrade/freqai/freqai_interface.py b/freqtrade/freqai/freqai_interface.py index b17f8c9ab..a9c21fb65 100644 --- a/freqtrade/freqai/freqai_interface.py +++ b/freqtrade/freqai/freqai_interface.py @@ -225,8 +225,8 @@ class IFreqaiModel(ABC): train_it += 1 total_trains = len(dk.backtesting_timeranges) self.training_timerange = tr_train - dataframe_train = dk.slice_dataframe_backtesting(tr_train, dataframe) - dataframe_backtest = dk.slice_dataframe_backtesting(tr_backtest, dataframe) + dataframe_train = dk.slice_dataframe(tr_train, dataframe) + dataframe_backtest = dk.slice_dataframe(tr_backtest, dataframe) trained_timestamp = tr_train tr_train_startts_str = datetime.datetime.utcfromtimestamp(tr_train.startts).strftime( From c8a9ac900c2bdec9620c6d2745ee78a3d78cfbdc Mon Sep 17 00:00:00 2001 From: robcaulk Date: Mon, 5 Sep 2022 00:52:26 +0200 Subject: [PATCH 20/96] fix broken CI --- tests/freqai/test_freqai_datakitchen.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/freqai/test_freqai_datakitchen.py b/tests/freqai/test_freqai_datakitchen.py index 9ef955695..44c721515 100644 --- a/tests/freqai/test_freqai_datakitchen.py +++ b/tests/freqai/test_freqai_datakitchen.py @@ -74,7 +74,7 @@ def test_use_DBSCAN_to_remove_outliers(mocker, freqai_conf, caplog): # freqai_conf['freqai']['feature_parameters'].update({"outlier_protection_percentage": 1}) freqai.dk.use_DBSCAN_to_remove_outliers(predict=False) assert log_has_re( - "DBSCAN found eps of 2.42.", + "DBSCAN found eps of 2.36.", caplog, ) @@ -83,7 +83,7 @@ def test_compute_distances(mocker, freqai_conf): freqai = make_data_dictionary(mocker, freqai_conf) freqai_conf['freqai']['feature_parameters'].update({"DI_threshold": 1}) avg_mean_dist = freqai.dk.compute_distances() - assert round(avg_mean_dist, 2) == 2.56 + assert round(avg_mean_dist, 2) == 2.54 def test_use_SVM_to_remove_outliers_and_outlier_protection(mocker, freqai_conf, caplog): @@ -91,6 +91,6 @@ def test_use_SVM_to_remove_outliers_and_outlier_protection(mocker, freqai_conf, freqai_conf['freqai']['feature_parameters'].update({"outlier_protection_percentage": 0.1}) freqai.dk.use_SVM_to_remove_outliers(predict=False) assert log_has_re( - "SVM detected 8.46%", + "SVM detected 8.09%", caplog, ) From a035a69a61e344fa5b7a7a01e2f01cfad46de7af Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Sep 2022 13:09:53 +0000 Subject: [PATCH 21/96] Bump arrow from 1.2.2 to 1.2.3 Bumps [arrow](https://github.com/arrow-py/arrow) from 1.2.2 to 1.2.3. - [Release notes](https://github.com/arrow-py/arrow/releases) - [Changelog](https://github.com/arrow-py/arrow/blob/master/CHANGELOG.rst) - [Commits](https://github.com/arrow-py/arrow/compare/1.2.2...1.2.3) --- updated-dependencies: - dependency-name: arrow dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index cbd5e31ba..99139f92b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ cryptography==37.0.4 aiohttp==3.8.1 SQLAlchemy==1.4.40 python-telegram-bot==13.13 -arrow==1.2.2 +arrow==1.2.3 cachetools==4.2.2 requests==2.28.1 urllib3==1.26.12 From 7cc8ac0a3429f0fd40c2be30974457f32259d744 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Sep 2022 13:10:02 +0000 Subject: [PATCH 22/96] Bump psutil from 5.9.1 to 5.9.2 Bumps [psutil](https://github.com/giampaolo/psutil) from 5.9.1 to 5.9.2. - [Release notes](https://github.com/giampaolo/psutil/releases) - [Changelog](https://github.com/giampaolo/psutil/blob/master/HISTORY.rst) - [Commits](https://github.com/giampaolo/psutil/compare/release-5.9.1...release-5.9.2) --- updated-dependencies: - dependency-name: psutil dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index cbd5e31ba..240eb0057 100644 --- a/requirements.txt +++ b/requirements.txt @@ -38,7 +38,7 @@ fastapi==0.81.0 uvicorn==0.18.3 pyjwt==2.4.0 aiofiles==0.8.0 -psutil==5.9.1 +psutil==5.9.2 # Support for colorized terminal output colorama==0.4.5 From af7e4d7bf0a5ce6f47e7a46d6f55dd814a43e22d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Sep 2022 13:10:11 +0000 Subject: [PATCH 23/96] Bump pytest from 7.1.2 to 7.1.3 Bumps [pytest](https://github.com/pytest-dev/pytest) from 7.1.2 to 7.1.3. - [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.2...7.1.3) --- 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 26df7115c..40ca4e154 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -10,7 +10,7 @@ flake8==5.0.4 flake8-tidy-imports==4.8.0 mypy==0.971 pre-commit==2.20.0 -pytest==7.1.2 +pytest==7.1.3 pytest-asyncio==0.19.0 pytest-cov==3.0.0 pytest-mock==3.8.2 From 3d038568457a7cfadfc0765ecc1e2ca5867f04e1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Sep 2022 13:10:31 +0000 Subject: [PATCH 24/96] Bump ccxt from 1.92.84 to 1.93.3 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.92.84 to 1.93.3. - [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.92.84...1.93.3) --- 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 cbd5e31ba..d70eebdd8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ numpy==1.23.2 pandas==1.4.3 pandas-ta==0.3.14b -ccxt==1.92.84 +ccxt==1.93.3 # Pin cryptography for now due to rust build errors with piwheels cryptography==37.0.4 aiohttp==3.8.1 From 90fbb794712cce946ae65caeacafa0f8724ae691 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Sep 2022 13:10:44 +0000 Subject: [PATCH 25/96] Bump prompt-toolkit from 3.0.30 to 3.0.31 Bumps [prompt-toolkit](https://github.com/prompt-toolkit/python-prompt-toolkit) from 3.0.30 to 3.0.31. - [Release notes](https://github.com/prompt-toolkit/python-prompt-toolkit/releases) - [Changelog](https://github.com/prompt-toolkit/python-prompt-toolkit/blob/master/CHANGELOG) - [Commits](https://github.com/prompt-toolkit/python-prompt-toolkit/compare/3.0.30...3.0.31) --- updated-dependencies: - dependency-name: prompt-toolkit dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index cbd5e31ba..ff1eabe38 100644 --- a/requirements.txt +++ b/requirements.txt @@ -44,7 +44,7 @@ psutil==5.9.1 colorama==0.4.5 # Building config files interactively questionary==1.10.0 -prompt-toolkit==3.0.30 +prompt-toolkit==3.0.31 # Extensions to datetime library python-dateutil==2.8.2 From 6f6afca0277de0ffeab2d104fb4b1068446dafc3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Sep 2022 13:10:53 +0000 Subject: [PATCH 26/96] Bump fastapi from 0.81.0 to 0.82.0 Bumps [fastapi](https://github.com/tiangolo/fastapi) from 0.81.0 to 0.82.0. - [Release notes](https://github.com/tiangolo/fastapi/releases) - [Commits](https://github.com/tiangolo/fastapi/compare/0.81.0...0.82.0) --- updated-dependencies: - dependency-name: fastapi 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 cbd5e31ba..2ddf3fbd9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -34,7 +34,7 @@ orjson==3.8.0 sdnotify==0.3.2 # API Server -fastapi==0.81.0 +fastapi==0.82.0 uvicorn==0.18.3 pyjwt==2.4.0 aiofiles==0.8.0 From f5500350f9364fd619415e56cd9d43eb42911f78 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Sep 2022 13:11:24 +0000 Subject: [PATCH 27/96] Bump pandas from 1.4.3 to 1.4.4 Bumps [pandas](https://github.com/pandas-dev/pandas) from 1.4.3 to 1.4.4. - [Release notes](https://github.com/pandas-dev/pandas/releases) - [Changelog](https://github.com/pandas-dev/pandas/blob/main/RELEASE.md) - [Commits](https://github.com/pandas-dev/pandas/compare/v1.4.3...v1.4.4) --- updated-dependencies: - dependency-name: pandas dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index cbd5e31ba..6bd828059 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ numpy==1.23.2 -pandas==1.4.3 +pandas==1.4.4 pandas-ta==0.3.14b ccxt==1.92.84 From 48dc1f2d88bf82f5721aba621b6768cc953f6ca9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Sep 2022 13:11:29 +0000 Subject: [PATCH 28/96] Bump pycoingecko from 2.2.0 to 3.0.0 Bumps [pycoingecko](https://github.com/man-c/pycoingecko) from 2.2.0 to 3.0.0. - [Release notes](https://github.com/man-c/pycoingecko/releases) - [Changelog](https://github.com/man-c/pycoingecko/blob/master/CHANGELOG.md) - [Commits](https://github.com/man-c/pycoingecko/compare/2.2.0...3.0.0) --- updated-dependencies: - dependency-name: pycoingecko dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index cbd5e31ba..20796bd26 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,7 +16,7 @@ jsonschema==4.14.0 TA-Lib==0.4.24 technical==1.3.0 tabulate==0.8.10 -pycoingecko==2.2.0 +pycoingecko==3.0.0 jinja2==3.1.2 tables==3.7.0 blosc==1.10.6 From 9f5642fd972affbf12f16859e391a3f243bc45ad Mon Sep 17 00:00:00 2001 From: Italo <45588475+italodamato@users.noreply.github.com> Date: Mon, 5 Sep 2022 18:12:19 +0200 Subject: [PATCH 29/96] fix hyperopt df preprocessing --- 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 fea2a672f..4c8613a7a 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -423,7 +423,7 @@ class Hyperopt: # Trim startup period from analyzed dataframe to get correct dates for output. processed = trim_dataframes(preprocessed, self.timerange, self.backtesting.required_startup) self.min_date, self.max_date = get_timerange(processed) - return processed + return preprocessed def prepare_hyperopt_data(self) -> None: HyperoptStateContainer.set_state(HyperoptState.DATALOAD) From 949f618d42530d4c4ca3e6a06eba5b99443a8bd1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Sep 2022 16:30:43 +0000 Subject: [PATCH 30/96] Bump python-telegram-bot from 13.13 to 13.14 Bumps [python-telegram-bot](https://github.com/python-telegram-bot/python-telegram-bot) from 13.13 to 13.14. - [Release notes](https://github.com/python-telegram-bot/python-telegram-bot/releases) - [Changelog](https://github.com/python-telegram-bot/python-telegram-bot/blob/v13.14/CHANGES.rst) - [Commits](https://github.com/python-telegram-bot/python-telegram-bot/compare/v13.13...v13.14) --- updated-dependencies: - dependency-name: python-telegram-bot 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 c7d55ba78..3cb321b2b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ ccxt==1.93.3 cryptography==37.0.4 aiohttp==3.8.1 SQLAlchemy==1.4.40 -python-telegram-bot==13.13 +python-telegram-bot==13.14 arrow==1.2.3 cachetools==4.2.2 requests==2.28.1 From 4628bfa580c49d8f80bf1372b66c95f6557b683c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Sep 2022 17:30:55 +0000 Subject: [PATCH 31/96] Bump jsonschema from 4.14.0 to 4.15.0 Bumps [jsonschema](https://github.com/python-jsonschema/jsonschema) from 4.14.0 to 4.15.0. - [Release notes](https://github.com/python-jsonschema/jsonschema/releases) - [Changelog](https://github.com/python-jsonschema/jsonschema/blob/main/CHANGELOG.rst) - [Commits](https://github.com/python-jsonschema/jsonschema/compare/v4.14.0...v4.15.0) --- updated-dependencies: - dependency-name: jsonschema 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 8b994cb88..fdbe6ac28 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ arrow==1.2.3 cachetools==4.2.2 requests==2.28.1 urllib3==1.26.12 -jsonschema==4.14.0 +jsonschema==4.15.0 TA-Lib==0.4.24 technical==1.3.0 tabulate==0.8.10 From 36e5c18fa600a3dbf3c58f6e2e7ee149a5b71ede Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 5 Sep 2022 20:54:03 +0200 Subject: [PATCH 32/96] Don't raise exception when a message is not implemented in telegram --- freqtrade/rpc/telegram.py | 12 +++++++----- tests/rpc/test_rpc_telegram.py | 10 +++++----- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 8c988d570..4a759f6ec 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -6,6 +6,7 @@ This module manage Telegram communication import json import logging import re +from copy import deepcopy from dataclasses import dataclass from datetime import date, datetime, timedelta from functools import partial @@ -374,7 +375,7 @@ class Telegram(RPCHandler): message += f"\n*Duration:* `{msg['duration']} ({msg['duration_min']:.1f} min)`" return message - def compose_message(self, msg: Dict[str, Any], msg_type: RPCMessageType) -> str: + def compose_message(self, msg: Dict[str, Any], msg_type: RPCMessageType) -> Optional[str]: if msg_type in [RPCMessageType.ENTRY, RPCMessageType.ENTRY_FILL]: message = self._format_entry_msg(msg) @@ -411,7 +412,8 @@ class Telegram(RPCHandler): elif msg_type == RPCMessageType.STRATEGY_MSG: message = f"{msg['msg']}" else: - raise NotImplementedError(f"Unknown message type: {msg_type}") + logger.debug("Unknown message type: %s", msg_type) + return None return message def send_msg(self, msg: Dict[str, Any]) -> None: @@ -438,9 +440,9 @@ class Telegram(RPCHandler): # Notification disabled return - message = self.compose_message(msg, msg_type) - - self._send_msg(message, disable_notification=(noti == 'silent')) + message = self.compose_message(deepcopy(msg), msg_type) + if message: + self._send_msg(message, disable_notification=(noti == 'silent')) def _get_sell_emoji(self, msg): """ diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index cde7025a7..f2e490dff 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -2138,11 +2138,11 @@ def test_send_msg_strategy_msg_notification(default_conf, mocker) -> None: def test_send_msg_unknown_type(default_conf, mocker) -> None: - telegram, _, _ = get_telegram_testobject(mocker, default_conf) - with pytest.raises(NotImplementedError, match=r'Unknown message type: None'): - telegram.send_msg({ - 'type': None, - }) + telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) + telegram.send_msg({ + 'type': None, + }) + msg_mock.call_count == 0 @pytest.mark.parametrize('message_type,enter,enter_signal,leverage', [ From 689b193240a77dc2e9619583a9bef3c9c7f07b56 Mon Sep 17 00:00:00 2001 From: robcaulk Date: Mon, 5 Sep 2022 20:57:42 +0200 Subject: [PATCH 33/96] improve clarity on data download requirements --- docs/freqai.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/freqai.md b/docs/freqai.md index 3646362c3..d04d2fa6b 100644 --- a/docs/freqai.md +++ b/docs/freqai.md @@ -280,7 +280,7 @@ The FreqAI strategy requires the user to include the following lines of code in Notice how the `populate_any_indicators()` is where the user adds their own features ([more information](#feature-engineering)) and labels ([more information](#setting-classifier-targets)). See a full example at `templates/FreqaiExampleStrategy.py`. ### Setting the `startup_candle_count` -Users need to take care to set the `startup_candle_count` in their strategy the same way they would for any normal Freqtrade strategy (see details [here](strategy-customization.md/#strategy-startup-period)). This value is used by Freqtrade to ensure that a sufficient amount of data is provided when calling on the `dataprovider` to avoid any NaNs at the beginning of the first training. Users can easily set this value by identifying the longest period (in candle units) that they pass to their indicator creation functions (e.g. talib functions). In the present example, the user would pass 20 to as this value (since it is the maximum value in their `indicators_periods_candles`). +Users need to take care to set the `startup_candle_count` in their strategy the same way they would for any normal Freqtrade strategy (see details [here](strategy-customization.md#strategy-startup-period)). This value is used by Freqtrade to ensure that a sufficient amount of data is provided when calling on the `dataprovider` to avoid any NaNs at the beginning of the first training. Users can easily set this value by identifying the longest period (in candle units) that they pass to their indicator creation functions (e.g. talib functions). In the present example, the user would pass 20 to as this value (since it is the maximum value in their `indicators_periods_candles`). !!! Note Typically it is best for users to be safe and multiply their expected `startup_candle_count` by 2. There are instances where the talib functions actually require more data than just the passed `period`. Anecdotally, multiplying the `startup_candle_count` by 2 always leads to a fully NaN free training dataset. Look out for this log message to confirm that your data is clean: @@ -515,10 +515,11 @@ and if a full `live_retrain_hours` has elapsed since the end of the loaded model The FreqAI backtesting module can be executed with the following command: ```bash -freqtrade backtesting --strategy FreqaiExampleStrategy --config config_examples/config_freqai.example.json --freqaimodel LightGBMRegressor --timerange 20210501-20210701 +freqtrade download-data --config config_examples/config_freqai.example.json --strategy FreqaiExampleStrategy --strategy-path freqtrade/templates -t 3m 15m 1h --timerange 20210315-20210701 +freqtrade backtesting --strategy FreqaiExampleStrategy --strategy-path freqtrade/templates --config config_examples/config_freqai.example.json --freqaimodel LightGBMRegressor --timerange 20210501-20210701 ``` -Backtesting mode requires the user to have the data pre-downloaded (unlike in dry/live mode where FreqAI automatically downloads the necessary data). The user should be careful to consider that the time range of the downloaded data is more than the backtesting time range. This is because FreqAI needs data prior to the desired backtesting time range in order to train a model to be ready to make predictions on the first candle of the user-set backtesting time range. More details on how to calculate the data to download can be found [here](#deciding-the-sliding-training-window-and-backtesting-duration). +Backtesting mode requires the user to have the data [pre-downloaded](#downloading-data-for-backtesting) (unlike in dry/live mode where FreqAI automatically downloads the necessary data). The user should be careful to consider that the time range of the downloaded data is more than the backtesting time range. This is because FreqAI needs data prior to the desired backtesting time range in order to train a model to be ready to make predictions on the first candle of the user-set backtesting time range. More details on how to calculate the data to download can be found [here](#deciding-the-sliding-training-window-and-backtesting-duration). If this command has never been executed with the existing config file, it will train a new model for each pair, for each backtesting window within the expanded `--timerange`. @@ -546,7 +547,7 @@ FreqAI will train have trained 8 separate models at the end of `--timerange` (be Although fractional `backtest_period_days` is allowed, the user should be aware that the `--timerange` is divided by this value to determine the number of models that FreqAI will need to train in order to backtest the full range. For example, if the user wants to set a `--timerange` of 10 days, and asks for a `backtest_period_days` of 0.1, FreqAI will need to train 100 models per pair to complete the full backtest. Because of this, a true backtest of FreqAI adaptive training would take a *very* long time. The best way to fully test a model is to run it dry and let it constantly train. In this case, backtesting would take the exact same amount of time as a dry run. ### Downloading data for backtesting -Live/dry instances will download the data automatically for the user, but users who wish to use backtesting functionality still need to download the necessary data using `download-data` (details [here](data-download/#data-downloading)). FreqAI users need to pay careful attention to understanding how much *additional* data needs to be downloaded to ensure that they have a sufficient amount of training data *before* the start of their backtesting timerange. The amount of additional data can be roughly estimated by taking subtracting `train_period_days` and the `startup_candle_count` ([details](#setting-the-startupcandlecount)) from the beginning of the desired backtesting timerange. +Live/dry instances will download the data automatically for the user, but users who wish to use backtesting functionality still need to download the necessary data using `download-data` (details [here](data-download.md#data-downloading)). FreqAI users need to pay careful attention to understanding how much *additional* data needs to be downloaded to ensure that they have a sufficient amount of training data *before* the start of their backtesting timerange. The amount of additional data can be roughly estimated by moving the start date of the timerange backwards by `train_period_days` and the `startup_candle_count` ([details](#setting-the-startupcandlecount)) from the beginning of the desired backtesting timerange. As an example, if we wish to backtest the `--timerange` above of `20210501-20210701`, and we use the example config which sets `train_period_days` to 15. The startup candle count is 40 on a maximum `include_timeframes` of 1h. We would need 20210501 - 15 days - 40 * 1h / 24 hours = 20210414 (16.7 days earlier than the start of the desired training timerange). From 9fb3517adc9b6d078fe3fc00d7393a79fcddae15 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 5 Sep 2022 21:08:01 +0200 Subject: [PATCH 34/96] Fix non-resolvable freqai test this test could never succeed in UI's as the name was constantly changing. --- tests/freqai/test_freqai_datakitchen.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/tests/freqai/test_freqai_datakitchen.py b/tests/freqai/test_freqai_datakitchen.py index 44c721515..908223c20 100644 --- a/tests/freqai/test_freqai_datakitchen.py +++ b/tests/freqai/test_freqai_datakitchen.py @@ -1,5 +1,5 @@ -import datetime import shutil +from datetime import datetime, timedelta, timezone from pathlib import Path import pytest @@ -56,16 +56,13 @@ def test_split_timerange( shutil.rmtree(Path(dk.full_path)) -@pytest.mark.parametrize( - "timestamp, expected", - [ - (datetime.datetime.now(tz=datetime.timezone.utc).timestamp() - 7200, True), - (datetime.datetime.now(tz=datetime.timezone.utc).timestamp(), False), - ], -) -def test_check_if_model_expired(mocker, freqai_conf, timestamp, expected): +def test_check_if_model_expired(mocker, freqai_conf): + dk = get_patched_data_kitchen(mocker, freqai_conf) - assert dk.check_if_model_expired(timestamp) == expected + now = datetime.now(tz=timezone.utc).timestamp() + assert dk.check_if_model_expired(now) is False + now = (datetime.now(tz=timezone.utc) - timedelta(hours=2)).timestamp() + assert dk.check_if_model_expired(now) is True shutil.rmtree(Path(dk.full_path)) From 1ea703d527b341322a64d0f8e2327117a2bca7a8 Mon Sep 17 00:00:00 2001 From: robcaulk Date: Mon, 5 Sep 2022 22:20:38 +0200 Subject: [PATCH 35/96] remove download-data command --- docs/freqai.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/freqai.md b/docs/freqai.md index d04d2fa6b..303c2f151 100644 --- a/docs/freqai.md +++ b/docs/freqai.md @@ -515,7 +515,6 @@ and if a full `live_retrain_hours` has elapsed since the end of the loaded model The FreqAI backtesting module can be executed with the following command: ```bash -freqtrade download-data --config config_examples/config_freqai.example.json --strategy FreqaiExampleStrategy --strategy-path freqtrade/templates -t 3m 15m 1h --timerange 20210315-20210701 freqtrade backtesting --strategy FreqaiExampleStrategy --strategy-path freqtrade/templates --config config_examples/config_freqai.example.json --freqaimodel LightGBMRegressor --timerange 20210501-20210701 ``` From 5b826150dff2c2d0f04b4a93ab802a5ce8b50c63 Mon Sep 17 00:00:00 2001 From: Wagner Costa Santos Date: Mon, 5 Sep 2022 17:43:28 -0300 Subject: [PATCH 36/96] fix hyperopt - freqai --- freqtrade/freqai/data_drawer.py | 29 +++++++++++++++++++++++----- freqtrade/freqai/freqai_interface.py | 7 +++++++ 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/freqtrade/freqai/data_drawer.py b/freqtrade/freqai/data_drawer.py index 9eeabef8f..a1ecb7654 100644 --- a/freqtrade/freqai/data_drawer.py +++ b/freqtrade/freqai/data_drawer.py @@ -93,6 +93,16 @@ class FreqaiDataDrawer: "model_filename": "", "trained_timestamp": 0, "priority": 1, "first": True, "data_path": "", "extras": {}} + def __getstate__(self): + """ + Return state values to be pickled. + It's necessary to allow serialization in hyperopt + """ + return ({ + "pair_dict": self.pair_dict, + "pair_dictionary_path": self.pair_dictionary_path + }) + def load_drawer_from_disk(self): """ Locate and load a previously saved data drawer full of all pair model metadata in @@ -155,14 +165,23 @@ class FreqaiDataDrawer: # create a backup shutil.copy(self.historic_predictions_path, self.historic_predictions_bkp_path) - def save_drawer_to_disk(self): + def save_drawer_to_disk(self, live=False): """ Save data drawer full of all pair model metadata in present model folder. """ - with self.save_lock: + if live: + with self.save_lock: + with open(self.pair_dictionary_path, 'w') as fp: + rapidjson.dump( + self.pair_dict, fp, default=self.np_encoder, + number_mode=rapidjson.NM_NATIVE + ) + else: + # save_lock it's not working with hyperopt with open(self.pair_dictionary_path, 'w') as fp: - rapidjson.dump(self.pair_dict, fp, default=self.np_encoder, - number_mode=rapidjson.NM_NATIVE) + rapidjson.dump( + self.pair_dict, fp, default=self.np_encoder, + number_mode=rapidjson.NM_NATIVE) def save_follower_dict_to_disk(self): """ @@ -437,7 +456,7 @@ class FreqaiDataDrawer: self.model_dictionary[coin] = model self.pair_dict[coin]["model_filename"] = dk.model_filename self.pair_dict[coin]["data_path"] = str(dk.data_path) - self.save_drawer_to_disk() + self.save_drawer_to_disk(dk.live) return diff --git a/freqtrade/freqai/freqai_interface.py b/freqtrade/freqai/freqai_interface.py index a9c21fb65..9a05e8383 100644 --- a/freqtrade/freqai/freqai_interface.py +++ b/freqtrade/freqai/freqai_interface.py @@ -90,6 +90,13 @@ class IFreqaiModel(ABC): self._threads: List[threading.Thread] = [] self._stop_event = threading.Event() + def __getstate__(self): + """ + Return state values to be pickled. + It's necessary to allow serialization in hyperopt + """ + return ({"dd": self.dd}) + def assert_config(self, config: Dict[str, Any]) -> None: if not config.get("freqai", {}): From 2c8e5b191bf84580684bf3a372121cef2e13d8f8 Mon Sep 17 00:00:00 2001 From: Wagner Costa Santos Date: Mon, 5 Sep 2022 17:43:55 -0300 Subject: [PATCH 37/96] fix hyperopt - freqai --- freqtrade/freqai/data_drawer.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/freqtrade/freqai/data_drawer.py b/freqtrade/freqai/data_drawer.py index a1ecb7654..dff6b5942 100644 --- a/freqtrade/freqai/data_drawer.py +++ b/freqtrade/freqai/data_drawer.py @@ -174,8 +174,7 @@ class FreqaiDataDrawer: with open(self.pair_dictionary_path, 'w') as fp: rapidjson.dump( self.pair_dict, fp, default=self.np_encoder, - number_mode=rapidjson.NM_NATIVE - ) + number_mode=rapidjson.NM_NATIVE) else: # save_lock it's not working with hyperopt with open(self.pair_dictionary_path, 'w') as fp: From 4dec19de9f9ca55565c7972c62eb65afa33d1f48 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 6 Sep 2022 06:52:50 +0200 Subject: [PATCH 38/96] Add comment to explain why we use the non-trimmed DF --- freqtrade/optimize/hyperopt.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 4c8613a7a..3becf857f 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -421,8 +421,9 @@ class Hyperopt: preprocessed = self.backtesting.strategy.advise_all_indicators(data) # Trim startup period from analyzed dataframe to get correct dates for output. - processed = trim_dataframes(preprocessed, self.timerange, self.backtesting.required_startup) - self.min_date, self.max_date = get_timerange(processed) + trimmed = trim_dataframes(preprocessed, self.timerange, self.backtesting.required_startup) + self.min_date, self.max_date = get_timerange(trimmed) + # Real trimming will happen as part of backtesting. return preprocessed def prepare_hyperopt_data(self) -> None: From 8597b52e345abca6111869bb5da71c01ce7d4107 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 6 Sep 2022 16:29:24 +0200 Subject: [PATCH 39/96] Slightly update docs to link to full sample --- docs/strategy-customization.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index a452b8f05..2b6e1fb2f 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -824,6 +824,8 @@ Options: - Merge the dataframe without lookahead bias - Forward-fill (optional) +For a full sample, please refer to the [complete data provider example](#complete-data-provider-sample) below. + All columns of the informative dataframe will be available on the returning dataframe in a renamed fashion: !!! Example "Column renaming" From 4b28d0495f46337700686f1b430827336285511a Mon Sep 17 00:00:00 2001 From: robcaulk Date: Tue, 6 Sep 2022 19:46:58 +0200 Subject: [PATCH 40/96] fix timestamping, move imports, add words to doc --- docs/freqai.md | 2 +- freqtrade/freqai/data_kitchen.py | 26 ++++++++++++-------------- freqtrade/freqai/freqai_interface.py | 15 +++++++-------- tests/freqai/conftest.py | 12 +++++------- 4 files changed, 25 insertions(+), 30 deletions(-) diff --git a/docs/freqai.md b/docs/freqai.md index d504c93d6..a8379106a 100644 --- a/docs/freqai.md +++ b/docs/freqai.md @@ -116,7 +116,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `outlier_protection_percentage` | If more than `outlier_protection_percentage` fraction of points are removed as outliers, FreqAI will log a warning message and ignore outlier detection while keeping the original dataset intact.
**Datatype:** float. Default: `30` | `reverse_train_test_order` | If true, FreqAI will train on the latest data split and test on historical split of the data. This allows the model to be trained up to the most recent data point, while avoiding overfitting. However, users should be careful to understand unorthodox nature of this parameter before employing it.
**Datatype:** bool. Default: False | `inlier_metric_window` | If set, FreqAI will add the `inlier_metric` to the training feature set and set the lookback to be the `inlier_metric_window`. Details of how the `inlier_metric` is computed can be found [here](#using-the-inliermetric)
**Datatype:** int. Default: 0 -| `noise_standard_deviation` | If > 0, FreqAI adds noise to the training features. FreqAI generates random deviates from a gaussian distribution with a standard deviation of `noise_standard_deviation` and adds them to all data points. Value should be kept relative to the normalized space between -1 and 1). In other words, since data is always normalized between -1 and 1 in FreqAI, the user can expect a `noise_standard_deviation: 0.05` to see 32% of data randomly increased/decreased by more than 2.5%. Good for preventing overfitting.
**Datatype:** int. Default: 0 +| `noise_standard_deviation` | If > 0, FreqAI adds noise to the training features. FreqAI generates random deviates from a gaussian distribution with a standard deviation of `noise_standard_deviation` and adds them to all data points. Value should be kept relative to the normalized space between -1 and 1). In other words, since data is always normalized between -1 and 1 in FreqAI, the user can expect a `noise_standard_deviation: 0.05` to see 32% of data randomly increased/decreased by more than 2.5% (i.e. the percent of data falling within the first standard deviation). Good for preventing overfitting.
**Datatype:** int. Default: 0 | | **Data split parameters** | `data_split_parameters` | Include any additional parameters available from Scikit-learn `test_train_split()`, which are shown [here](https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.train_test_split.html) (external website).
**Datatype:** Dictionary. | `test_size` | Fraction of data that should be used for testing instead of training.
**Datatype:** Positive float < 1. diff --git a/freqtrade/freqai/data_kitchen.py b/freqtrade/freqai/data_kitchen.py index fce9e8480..8ef2d6aea 100644 --- a/freqtrade/freqai/data_kitchen.py +++ b/freqtrade/freqai/data_kitchen.py @@ -2,12 +2,14 @@ import copy import logging import shutil from datetime import datetime, timezone +from math import cos, sin from pathlib import Path from typing import Any, Dict, List, Tuple import numpy as np import numpy.typing as npt import pandas as pd +import scipy.stats as stats from pandas import DataFrame from sklearn import linear_model from sklearn.cluster import DBSCAN @@ -401,8 +403,8 @@ class FreqaiDataKitchen: timerange_train.stopts = timerange_train.startts + train_period_days first = False - start = datetime.utcfromtimestamp(timerange_train.startts) - stop = datetime.utcfromtimestamp(timerange_train.stopts) + start = datetime.fromtimestamp(timerange_train.startts, tz=timezone.utc) + stop = datetime.fromtimestamp(timerange_train.stopts, tz=timezone.utc) tr_training_list.append(start.strftime("%Y%m%d") + "-" + stop.strftime("%Y%m%d")) tr_training_list_timerange.append(copy.deepcopy(timerange_train)) @@ -415,8 +417,8 @@ class FreqaiDataKitchen: if timerange_backtest.stopts > config_timerange.stopts: timerange_backtest.stopts = config_timerange.stopts - start = datetime.utcfromtimestamp(timerange_backtest.startts) - stop = datetime.utcfromtimestamp(timerange_backtest.stopts) + start = datetime.fromtimestamp(timerange_backtest.startts, tz=timezone.utc) + stop = datetime.fromtimestamp(timerange_backtest.stopts, tz=timezone.utc) tr_backtesting_list.append(start.strftime("%Y%m%d") + "-" + stop.strftime("%Y%m%d")) tr_backtesting_list_timerange.append(copy.deepcopy(timerange_backtest)) @@ -630,8 +632,6 @@ class FreqaiDataKitchen: is an outlier. """ - from math import cos, sin - if predict: if not self.data['DBSCAN_eps']: return @@ -732,8 +732,6 @@ class FreqaiDataKitchen: into previous timepoints. """ - import scipy.stats as ss - no_prev_pts = self.freqai_config["feature_parameters"]["inlier_metric_window"] if set_ == 'train': @@ -778,8 +776,8 @@ class FreqaiDataKitchen: inliers = pd.DataFrame(index=distances.index) for key in distances.keys(): current_distances = distances[key].dropna() - fit_params = ss.weibull_min.fit(current_distances) - quantiles = ss.weibull_min.cdf(current_distances, *fit_params) + fit_params = stats.weibull_min.fit(current_distances) + quantiles = stats.weibull_min.cdf(current_distances, *fit_params) df_inlier = pd.DataFrame( {key: quantiles}, index=distances.index @@ -794,8 +792,8 @@ class FreqaiDataKitchen: index=compute_df.index ) - inlier_metric = 2 * (inlier_metric - inlier_metric.min()) / \ - (inlier_metric.max() - inlier_metric.min()) - 1 + inlier_metric = (2 * (inlier_metric - inlier_metric.min()) / + (inlier_metric.max() - inlier_metric.min()) - 1) if set_ in ('train', 'test'): inlier_metric = inlier_metric.iloc[no_prev_pts:] @@ -956,8 +954,8 @@ class FreqaiDataKitchen: backtest_timerange.startts = ( backtest_timerange.startts - backtest_period_days * SECONDS_IN_DAY ) - start = datetime.utcfromtimestamp(backtest_timerange.startts) - stop = datetime.utcfromtimestamp(backtest_timerange.stopts) + start = datetime.fromtimestamp(backtest_timerange.startts, tz=timezone.utc) + stop = datetime.fromtimestamp(backtest_timerange.stopts, tz=timezone.utc) full_timerange = start.strftime("%Y%m%d") + "-" + stop.strftime("%Y%m%d") self.full_path = Path( diff --git a/freqtrade/freqai/freqai_interface.py b/freqtrade/freqai/freqai_interface.py index fd0554248..9b3e853ef 100644 --- a/freqtrade/freqai/freqai_interface.py +++ b/freqtrade/freqai/freqai_interface.py @@ -1,10 +1,9 @@ -# import contextlib import logging import shutil import threading import time from abc import ABC, abstractmethod -from datetime import datetime +from datetime import datetime, timezone from pathlib import Path from threading import Lock from typing import Any, Dict, Tuple @@ -214,12 +213,12 @@ class IFreqaiModel(ABC): dataframe_backtest = dk.slice_dataframe(tr_backtest, dataframe) trained_timestamp = tr_train - tr_train_startts_str = datetime.utcfromtimestamp(tr_train.startts).strftime( - "%Y-%m-%d %H:%M:%S" - ) - tr_train_stopts_str = datetime.utcfromtimestamp(tr_train.stopts).strftime( - "%Y-%m-%d %H:%M:%S" - ) + tr_train_startts_str = datetime.fromtimestamp( + tr_train.startts, + tz=timezone.utc).strftime("%Y-%m-%d %H:%M:%S") + tr_train_stopts_str = datetime.fromtimestamp( + tr_train.stopts, + tz=timezone.utc).strftime("%Y-%m-%d %H:%M:%S") logger.info( f"Training {metadata['pair']}, {self.pair_it}/{self.total_pairs} pairs" f" from {tr_train_startts_str} to {tr_train_stopts_str}, {train_it}/{total_trains} " diff --git a/tests/freqai/conftest.py b/tests/freqai/conftest.py index 6528347e8..ffdc52ebc 100644 --- a/tests/freqai/conftest.py +++ b/tests/freqai/conftest.py @@ -93,12 +93,11 @@ def make_unfiltered_dataframe(mocker, freqai_conf): freqai.live = True freqai.dk = FreqaiDataKitchen(freqai_conf) freqai.dk.pair = "ADA/BTC" - timerange = TimeRange.parse_timerange("20180110-20180130") - freqai.dd.load_all_pair_histories(timerange, freqai.dk) + data_load_timerange = TimeRange.parse_timerange("20180110-20180130") + freqai.dd.load_all_pair_histories(data_load_timerange, freqai.dk) freqai.dd.pair_dict = MagicMock() - - data_load_timerange = TimeRange.parse_timerange("20180110-20180130") + new_timerange = TimeRange.parse_timerange("20180120-20180130") corr_dataframes, base_dataframes = freqai.dd.get_base_and_corr_dataframes( @@ -125,12 +124,11 @@ def make_data_dictionary(mocker, freqai_conf): freqai.live = True freqai.dk = FreqaiDataKitchen(freqai_conf) freqai.dk.pair = "ADA/BTC" - timerange = TimeRange.parse_timerange("20180110-20180130") - freqai.dd.load_all_pair_histories(timerange, freqai.dk) + data_load_timerange = TimeRange.parse_timerange("20180110-20180130") + freqai.dd.load_all_pair_histories(data_load_timerange, freqai.dk) freqai.dd.pair_dict = MagicMock() - data_load_timerange = TimeRange.parse_timerange("20180110-20180130") new_timerange = TimeRange.parse_timerange("20180120-20180130") corr_dataframes, base_dataframes = freqai.dd.get_base_and_corr_dataframes( From f2f811a2feed0a8ff427d638ac773d6aea670eaa Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 6 Sep 2022 19:55:18 +0200 Subject: [PATCH 41/96] Fix telegram bug with open partial exit orders --- freqtrade/rpc/rpc.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 11311f671..05599074c 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -261,11 +261,15 @@ class RPC: profit_str += f" ({fiat_profit:.2f})" fiat_profit_sum = fiat_profit if isnan(fiat_profit_sum) \ else fiat_profit_sum + fiat_profit + open_order = (trade.select_order_by_order_id( + trade.open_order_id) if trade.open_order_id else None) + detail_trade = [ f'{trade.id} {direction_str}', - trade.pair + ('*' if (trade.open_order_id is not None - and trade.close_rate_requested is None) else '') - + ('**' if (trade.close_rate_requested is not None) else ''), + trade.pair + ('*' if (open_order + and open_order.ft_order_side == trade.entry_side) else '') + + ('**' if (open_order and + open_order.ft_order_side == trade.exit_side is not None) else ''), shorten_date(arrow.get(trade.open_date).humanize(only_distance=True)), profit_str ] From e83c9b276dab1659da3ea245792fe858b43ed054 Mon Sep 17 00:00:00 2001 From: robcaulk Date: Tue, 6 Sep 2022 19:56:52 +0200 Subject: [PATCH 42/96] fix whitespace --- freqtrade/freqai/data_kitchen.py | 2 +- tests/freqai/conftest.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqai/data_kitchen.py b/freqtrade/freqai/data_kitchen.py index 8ef2d6aea..471634c85 100644 --- a/freqtrade/freqai/data_kitchen.py +++ b/freqtrade/freqai/data_kitchen.py @@ -9,7 +9,7 @@ from typing import Any, Dict, List, Tuple import numpy as np import numpy.typing as npt import pandas as pd -import scipy.stats as stats +from scipy import stats from pandas import DataFrame from sklearn import linear_model from sklearn.cluster import DBSCAN diff --git a/tests/freqai/conftest.py b/tests/freqai/conftest.py index ffdc52ebc..2bd744455 100644 --- a/tests/freqai/conftest.py +++ b/tests/freqai/conftest.py @@ -97,7 +97,7 @@ def make_unfiltered_dataframe(mocker, freqai_conf): freqai.dd.load_all_pair_histories(data_load_timerange, freqai.dk) freqai.dd.pair_dict = MagicMock() - + new_timerange = TimeRange.parse_timerange("20180120-20180130") corr_dataframes, base_dataframes = freqai.dd.get_base_and_corr_dataframes( From 90ec336c70e02e0ae8156a768bf69b4ac0fa55b5 Mon Sep 17 00:00:00 2001 From: th0rntwig <107926911+th0rntwig@users.noreply.github.com> Date: Tue, 6 Sep 2022 19:58:25 +0200 Subject: [PATCH 43/96] Update+correct descriptions and figure (#7323) --- docs/assets/freqai_weight-factor.jpg | Bin 195825 -> 189828 bytes docs/freqai.md | 14 +++++++------- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/assets/freqai_weight-factor.jpg b/docs/assets/freqai_weight-factor.jpg index 4f8b23e1849588058fa8a55d5adf67466ca90c86..c7580787de79862f2af2539eb904682050614a53 100644 GIT binary patch literal 189828 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

atyK_sD60U=mK;s}+C5jk3N@uK`3!!+T-9Td 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%0DZ+e9?q9jI}lqMU6O_;grg|T!qwHDN+&5x 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

H^AUqc2mXm60lZX+5Q? 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%0DY|A%{7gkA4hXNH} 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 diff --git a/docs/freqai.md b/docs/freqai.md index 303c2f151..c0844bf32 100644 --- a/docs/freqai.md +++ b/docs/freqai.md @@ -112,15 +112,15 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `DI_threshold` | Activates the Dissimilarity Index for outlier detection when > 0. See details about how it works [here](#removing-outliers-with-the-dissimilarity-index).
**Datatype:** Positive float (typically < 1). | `use_SVM_to_remove_outliers` | Train a support vector machine to detect and remove outliers from the training data set, as well as from incoming data points. See details about how it works [here](#removing-outliers-using-a-support-vector-machine-svm).
**Datatype:** Boolean. | `svm_params` | All parameters available in Sklearn's `SGDOneClassSVM()`. See details about some select parameters [here](#removing-outliers-using-a-support-vector-machine-svm).
**Datatype:** Dictionary. -| `use_DBSCAN_to_remove_outliers` | Cluster data using DBSCAN to identify and remove outliers from training and prediction data. See details about how it works [here](#removing-outliers-with-dbscan).
**Datatype:** Boolean. -| `outlier_protection_percentage` | If more than `outlier_protection_percentage` fraction of points are removed as outliers, FreqAI will log a warning message and ignore outlier detection while keeping the original dataset intact.
**Datatype:** float. Default: `30` -| `reverse_train_test_order` | If true, FreqAI will train on the latest data split and test on historical split of the data. This allows the model to be trained up to the most recent data point, while avoiding overfitting. However, users should be careful to understand unorthodox nature of this parameter before employing it.
**Datatype:** bool. Default: False +| `use_DBSCAN_to_remove_outliers` | Cluster data using DBSCAN to identify and remove outliers from training and prediction data. See details about how it works [here](#removing-outliers-with-dbscan).
**Datatype:** Boolean. +| `outlier_protection_percentage` | If more than `outlier_protection_percentage` % of points are detected as outliers by the SVM or DBSCAN, FreqAI will log a warning message and ignore outlier detection while keeping the original dataset intact. If the outlier protection is triggered, no predictions will be made based on the training data.
**Datatype:** Float. Default: `30` +| `reverse_train_test_order` | If true, FreqAI will train on the latest data split and test on historical split of the data. This allows the model to be trained up to the most recent data point, while avoiding overfitting. However, users should be careful to understand unorthodox nature of this parameter before employing it.
**Datatype:** Boolean. Default: False | | **Data split parameters** | `data_split_parameters` | Include any additional parameters available from Scikit-learn `test_train_split()`, which are shown [here](https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.train_test_split.html) (external website).
**Datatype:** Dictionary. | `test_size` | Fraction of data that should be used for testing instead of training.
**Datatype:** Positive float < 1. -| `shuffle` | Shuffle the training data points during training. Typically, for time-series forecasting, this is set to `False`.
+| `shuffle` | Shuffle the training data points during training. Typically, for time-series forecasting, this is set to `False`.
**Datatype:** Boolean. | | **Model training parameters** -| `model_training_parameters` | A flexible dictionary that includes all parameters available by the user selected model library. For example, if the user uses `LightGBMRegressor`, this dictionary can contain any parameter available by the `LightGBMRegressor` [here](https://lightgbm.readthedocs.io/en/latest/pythonapi/lightgbm.LGBMRegressor.html) (external website). If the user selects a different model, this dictionary can contain any parameter from that model.
**Datatype:** Dictionary.**Datatype:** Boolean. +| `model_training_parameters` | A flexible dictionary that includes all parameters available by the user selected model library. For example, if the user uses `LightGBMRegressor`, this dictionary can contain any parameter available by the `LightGBMRegressor` [here](https://lightgbm.readthedocs.io/en/latest/pythonapi/lightgbm.LGBMRegressor.html) (external website). If the user selects a different model, this dictionary can contain any parameter from that model.
**Datatype:** Dictionary. | `n_estimators` | The number of boosted trees to fit in regression.
**Datatype:** Integer. | `learning_rate` | Boosting learning rate during regression.
**Datatype:** Float. | `n_jobs`, `thread_count`, `task_type` | Set the number of threads for parallel processing and the `task_type` (`gpu` or `cpu`). Different model libraries use different parameter names.
**Datatype:** Float. @@ -738,7 +738,7 @@ Given a number of data points $N$, and a distance $\varepsilon$, DBSCAN clusters ![dbscan](assets/freqai_dbscan.jpg) -FreqAI uses `sklearn.cluster.DBSCAN` (details are available on scikit-learn's webpage [here](#https://scikit-learn.org/stable/modules/generated/sklearn.cluster.DBSCAN.html)) with `min_samples` ($N$) taken as double the no. of user-defined features, and `eps` ($\varepsilon$) taken as the longest distance in the *k-distance graph* computed from the nearest neighbors in the pairwise distances of all data points in the feature set. +FreqAI uses `sklearn.cluster.DBSCAN` (details are available on scikit-learn's webpage [here](#https://scikit-learn.org/stable/modules/generated/sklearn.cluster.DBSCAN.html)) with `min_samples` ($N$) taken as 1/4 of the no. of time points in the feature set, and `eps` ($\varepsilon$) taken as the elbow point in the *k-distance graph* computed from the nearest neighbors in the pairwise distances of all data points in the feature set. ## Additional information @@ -763,5 +763,5 @@ Code review, software architecture brainstorming: @xmatthias Beta testing and bug reporting: -@bloodhunter4rc, Salah Lamkadem @ikonx, @ken11o2, @longyu, @paranoidandy, @smidelis, @smarm +@bloodhunter4rc, Salah Lamkadem @ikonx, @ken11o2, @longyu, @paranoidandy, @smidelis, @smarm, Juha Nykänen @suikula, Wagner Costa @wagnercosta From d44296783e6264e25a3c20cfc7f5e104883f8ce2 Mon Sep 17 00:00:00 2001 From: robcaulk Date: Tue, 6 Sep 2022 20:10:12 +0200 Subject: [PATCH 44/96] isort datakitchen --- freqtrade/freqai/data_kitchen.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqai/data_kitchen.py b/freqtrade/freqai/data_kitchen.py index 471634c85..2ed0d73af 100644 --- a/freqtrade/freqai/data_kitchen.py +++ b/freqtrade/freqai/data_kitchen.py @@ -9,8 +9,8 @@ from typing import Any, Dict, List, Tuple import numpy as np import numpy.typing as npt import pandas as pd -from scipy import stats from pandas import DataFrame +from scipy import stats from sklearn import linear_model from sklearn.cluster import DBSCAN from sklearn.metrics.pairwise import pairwise_distances From dc4a4bdf09cfc3ecb588cdabb3877e07b5762ab0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 6 Sep 2022 20:25:46 +0200 Subject: [PATCH 45/96] Wrap cleanup in try/finally handler If a database has errors, the database cleanups would fail, causing cleanup to be incomplete. closes #7364 --- freqtrade/freqtradebot.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index f7dca1d75..1d171ae89 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -142,17 +142,20 @@ class FreqtradeBot(LoggingMixin): :return: None """ logger.info('Cleaning up modules ...') + try: + # Wrap db activities in shutdown to avoid problems if database is gone, + # and raises further exceptions. + if self.config['cancel_open_orders_on_exit']: + self.cancel_all_open_orders() - if self.config['cancel_open_orders_on_exit']: - self.cancel_all_open_orders() + self.check_for_open_trades() - self.check_for_open_trades() + finally: + self.strategy.ft_bot_cleanup() - self.strategy.ft_bot_cleanup() - - self.rpc.cleanup() - Trade.commit() - self.exchange.close() + self.rpc.cleanup() + Trade.commit() + self.exchange.close() def startup(self) -> None: """ From 97077ba18acd9ea0ad67ba45e917aca6bdcb3b0d Mon Sep 17 00:00:00 2001 From: robcaulk Date: Tue, 6 Sep 2022 20:30:37 +0200 Subject: [PATCH 46/96] add continual learning to catboost and friends --- docs/freqai.md | 1 + freqtrade/freqai/freqai_interface.py | 3 ++- .../prediction_models/BaseClassifierModel.py | 2 +- .../prediction_models/BaseRegressionModel.py | 2 +- .../prediction_models/BaseTensorFlowModel.py | 2 +- .../prediction_models/CatboostClassifier.py | 11 ++++++++--- .../freqai/prediction_models/CatboostRegressor.py | 15 ++++++++------- .../CatboostRegressorMultiTarget.py | 7 +++++-- .../prediction_models/LightGBMClassifier.py | 11 ++++++++--- .../freqai/prediction_models/LightGBMRegressor.py | 11 ++++++++--- .../LightGBMRegressorMultiTarget.py | 7 +++++-- 11 files changed, 48 insertions(+), 24 deletions(-) diff --git a/docs/freqai.md b/docs/freqai.md index c0844bf32..e790bbb81 100644 --- a/docs/freqai.md +++ b/docs/freqai.md @@ -98,6 +98,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `expiration_hours` | Avoid making predictions if a model is more than `expiration_hours` old.
Defaults set to 0, which means models never expire.
**Datatype:** Positive integer. | `fit_live_predictions_candles` | Number of historical candles to use for computing target (label) statistics from prediction data, instead of from the training data set.
**Datatype:** Positive integer. | `follow_mode` | If true, this instance of FreqAI will look for models associated with `identifier` and load those for inferencing. A `follower` will **not** train new models.
**Datatype:** Boolean. Default: `False`. +| `continual_learning` | If true, FreqAI will start training new models from the final state of the most recently trained model.
**Datatype:** Boolean. Default: `False`. | | **Feature parameters** | `feature_parameters` | A dictionary containing the parameters used to engineer the feature set. Details and examples are shown [here](#feature-engineering).
**Datatype:** Dictionary. | `include_timeframes` | A list of timeframes that all indicators in `populate_any_indicators` will be created for. The list is added as features to the base asset feature set.
**Datatype:** List of timeframes (strings). diff --git a/freqtrade/freqai/freqai_interface.py b/freqtrade/freqai/freqai_interface.py index a9c21fb65..b6f3d8ebc 100644 --- a/freqtrade/freqai/freqai_interface.py +++ b/freqtrade/freqai/freqai_interface.py @@ -86,6 +86,7 @@ class IFreqaiModel(ABC): self.begin_time: float = 0 self.begin_time_train: float = 0 self.base_tf_seconds = timeframe_to_seconds(self.config['timeframe']) + self.continual_learning = self.freqai_info.get('continual_learning', False) self._threads: List[threading.Thread] = [] self._stop_event = threading.Event() @@ -674,7 +675,7 @@ class IFreqaiModel(ABC): """ @abstractmethod - def fit(self, data_dictionary: Dict[str, Any]) -> Any: + def fit(self, data_dictionary: Dict[str, Any], dk: FreqaiDataKitchen) -> Any: """ Most regressors use the same function names and arguments e.g. user can drop in LGBMRegressor in place of CatBoostRegressor and all data diff --git a/freqtrade/freqai/prediction_models/BaseClassifierModel.py b/freqtrade/freqai/prediction_models/BaseClassifierModel.py index 2edbf3b51..e51e26e0f 100644 --- a/freqtrade/freqai/prediction_models/BaseClassifierModel.py +++ b/freqtrade/freqai/prediction_models/BaseClassifierModel.py @@ -61,7 +61,7 @@ class BaseClassifierModel(IFreqaiModel): ) logger.info(f'Training model on {len(data_dictionary["train_features"])} data points') - model = self.fit(data_dictionary) + model = self.fit(data_dictionary, dk) logger.info(f"--------------------done training {pair}--------------------") diff --git a/freqtrade/freqai/prediction_models/BaseRegressionModel.py b/freqtrade/freqai/prediction_models/BaseRegressionModel.py index 2ef175a2e..45f0c2937 100644 --- a/freqtrade/freqai/prediction_models/BaseRegressionModel.py +++ b/freqtrade/freqai/prediction_models/BaseRegressionModel.py @@ -60,7 +60,7 @@ class BaseRegressionModel(IFreqaiModel): ) logger.info(f'Training model on {len(data_dictionary["train_features"])} data points') - model = self.fit(data_dictionary) + model = self.fit(data_dictionary, dk) logger.info(f"--------------------done training {pair}--------------------") diff --git a/freqtrade/freqai/prediction_models/BaseTensorFlowModel.py b/freqtrade/freqai/prediction_models/BaseTensorFlowModel.py index 04eff045f..66e6ec1fc 100644 --- a/freqtrade/freqai/prediction_models/BaseTensorFlowModel.py +++ b/freqtrade/freqai/prediction_models/BaseTensorFlowModel.py @@ -57,7 +57,7 @@ class BaseTensorFlowModel(IFreqaiModel): ) logger.info(f'Training model on {len(data_dictionary["train_features"])} data points') - model = self.fit(data_dictionary) + model = self.fit(data_dictionary, dk) logger.info(f"--------------------done training {pair}--------------------") diff --git a/freqtrade/freqai/prediction_models/CatboostClassifier.py b/freqtrade/freqai/prediction_models/CatboostClassifier.py index b88b28b25..13395879a 100644 --- a/freqtrade/freqai/prediction_models/CatboostClassifier.py +++ b/freqtrade/freqai/prediction_models/CatboostClassifier.py @@ -2,7 +2,7 @@ import logging from typing import Any, Dict from catboost import CatBoostClassifier, Pool - +from freqtrade.freqai.data_kitchen import FreqaiDataKitchen from freqtrade.freqai.prediction_models.BaseClassifierModel import BaseClassifierModel @@ -16,7 +16,7 @@ class CatboostClassifier(BaseClassifierModel): has its own DataHandler where data is held, saved, loaded, and managed. """ - def fit(self, data_dictionary: Dict) -> Any: + def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen) -> Any: """ User sets up the training and test data to fit their desired model here :params: @@ -36,6 +36,11 @@ class CatboostClassifier(BaseClassifierModel): **self.model_training_parameters, ) - cbr.fit(train_data) + if dk.pair not in self.dd.model_dictionary or not self.continual_learning: + init_model = None + else: + init_model = self.dd.model_dictionary[dk.pair] + + cbr.fit(train_data, init_model=init_model) return cbr diff --git a/freqtrade/freqai/prediction_models/CatboostRegressor.py b/freqtrade/freqai/prediction_models/CatboostRegressor.py index d93569c91..0b8bc162b 100644 --- a/freqtrade/freqai/prediction_models/CatboostRegressor.py +++ b/freqtrade/freqai/prediction_models/CatboostRegressor.py @@ -3,6 +3,7 @@ import logging from typing import Any, Dict from catboost import CatBoostRegressor, Pool +from freqtrade.freqai.data_kitchen import FreqaiDataKitchen from freqtrade.freqai.prediction_models.BaseRegressionModel import BaseRegressionModel @@ -17,7 +18,7 @@ class CatboostRegressor(BaseRegressionModel): has its own DataHandler where data is held, saved, loaded, and managed. """ - def fit(self, data_dictionary: Dict) -> Any: + def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen) -> Any: """ User sets up the training and test data to fit their desired model here :param data_dictionary: the dictionary constructed by DataHandler to hold @@ -38,16 +39,16 @@ class CatboostRegressor(BaseRegressionModel): weight=data_dictionary["test_weights"], ) + if dk.pair not in self.dd.model_dictionary or not self.continual_learning: + init_model = None + else: + init_model = self.dd.model_dictionary[dk.pair] + model = CatBoostRegressor( allow_writing_files=False, **self.model_training_parameters, ) - model.fit(X=train_data, eval_set=test_data) - - # some evidence that catboost pools have memory leaks: - # https://github.com/catboost/catboost/issues/1835 - del train_data, test_data - gc.collect() + model.fit(X=train_data, eval_set=test_data, init_model=init_model) return model diff --git a/freqtrade/freqai/prediction_models/CatboostRegressorMultiTarget.py b/freqtrade/freqai/prediction_models/CatboostRegressorMultiTarget.py index 9894decd1..9ed61488c 100644 --- a/freqtrade/freqai/prediction_models/CatboostRegressorMultiTarget.py +++ b/freqtrade/freqai/prediction_models/CatboostRegressorMultiTarget.py @@ -3,7 +3,7 @@ from typing import Any, Dict from catboost import CatBoostRegressor # , Pool from sklearn.multioutput import MultiOutputRegressor - +from freqtrade.freqai.data_kitchen import FreqaiDataKitchen from freqtrade.freqai.prediction_models.BaseRegressionModel import BaseRegressionModel @@ -17,7 +17,7 @@ class CatboostRegressorMultiTarget(BaseRegressionModel): has its own DataHandler where data is held, saved, loaded, and managed. """ - def fit(self, data_dictionary: Dict) -> Any: + def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen) -> Any: """ User sets up the training and test data to fit their desired model here :param data_dictionary: the dictionary constructed by DataHandler to hold @@ -34,6 +34,9 @@ class CatboostRegressorMultiTarget(BaseRegressionModel): eval_set = (data_dictionary["test_features"], data_dictionary["test_labels"]) sample_weight = data_dictionary["train_weights"] + if self.continual_learning: + logger.warning('Continual learning not supported for MultiTarget models') + model = MultiOutputRegressor(estimator=cbr) model.fit(X=X, y=y, sample_weight=sample_weight) # , eval_set=eval_set) diff --git a/freqtrade/freqai/prediction_models/LightGBMClassifier.py b/freqtrade/freqai/prediction_models/LightGBMClassifier.py index 4ac2c448b..0023a9f69 100644 --- a/freqtrade/freqai/prediction_models/LightGBMClassifier.py +++ b/freqtrade/freqai/prediction_models/LightGBMClassifier.py @@ -4,7 +4,7 @@ from typing import Any, Dict from lightgbm import LGBMClassifier from freqtrade.freqai.prediction_models.BaseClassifierModel import BaseClassifierModel - +from freqtrade.freqai.data_kitchen import FreqaiDataKitchen logger = logging.getLogger(__name__) @@ -16,7 +16,7 @@ class LightGBMClassifier(BaseClassifierModel): has its own DataHandler where data is held, saved, loaded, and managed. """ - def fit(self, data_dictionary: Dict) -> Any: + def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen) -> Any: """ User sets up the training and test data to fit their desired model here :params: @@ -35,9 +35,14 @@ class LightGBMClassifier(BaseClassifierModel): y = data_dictionary["train_labels"].to_numpy()[:, 0] train_weights = data_dictionary["train_weights"] + if dk.pair not in self.dd.model_dictionary or not self.continual_learning: + init_model = None + else: + init_model = self.dd.model_dictionary[dk.pair] + model = LGBMClassifier(**self.model_training_parameters) model.fit(X=X, y=y, eval_set=eval_set, sample_weight=train_weights, - eval_sample_weight=[test_weights]) + eval_sample_weight=[test_weights], init_model=init_model) return model diff --git a/freqtrade/freqai/prediction_models/LightGBMRegressor.py b/freqtrade/freqai/prediction_models/LightGBMRegressor.py index 2431fd2ad..81f0e6d22 100644 --- a/freqtrade/freqai/prediction_models/LightGBMRegressor.py +++ b/freqtrade/freqai/prediction_models/LightGBMRegressor.py @@ -4,7 +4,7 @@ from typing import Any, Dict from lightgbm import LGBMRegressor from freqtrade.freqai.prediction_models.BaseRegressionModel import BaseRegressionModel - +from freqtrade.freqai.data_kitchen import FreqaiDataKitchen logger = logging.getLogger(__name__) @@ -16,7 +16,7 @@ class LightGBMRegressor(BaseRegressionModel): has its own DataHandler where data is held, saved, loaded, and managed. """ - def fit(self, data_dictionary: Dict) -> Any: + def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen) -> Any: """ Most regressors use the same function names and arguments e.g. user can drop in LGBMRegressor in place of CatBoostRegressor and all data @@ -35,9 +35,14 @@ class LightGBMRegressor(BaseRegressionModel): y = data_dictionary["train_labels"] train_weights = data_dictionary["train_weights"] + if dk.pair not in self.dd.model_dictionary or not self.continual_learning: + init_model = None + else: + init_model = self.dd.model_dictionary[dk.pair] + model = LGBMRegressor(**self.model_training_parameters) model.fit(X=X, y=y, eval_set=eval_set, sample_weight=train_weights, - eval_sample_weight=[eval_weights]) + eval_sample_weight=[eval_weights], init_model=init_model) return model diff --git a/freqtrade/freqai/prediction_models/LightGBMRegressorMultiTarget.py b/freqtrade/freqai/prediction_models/LightGBMRegressorMultiTarget.py index ecd405369..2b25493e0 100644 --- a/freqtrade/freqai/prediction_models/LightGBMRegressorMultiTarget.py +++ b/freqtrade/freqai/prediction_models/LightGBMRegressorMultiTarget.py @@ -5,7 +5,7 @@ from lightgbm import LGBMRegressor from sklearn.multioutput import MultiOutputRegressor from freqtrade.freqai.prediction_models.BaseRegressionModel import BaseRegressionModel - +from freqtrade.freqai.data_kitchen import FreqaiDataKitchen logger = logging.getLogger(__name__) @@ -17,7 +17,7 @@ class LightGBMRegressorMultiTarget(BaseRegressionModel): has its own DataHandler where data is held, saved, loaded, and managed. """ - def fit(self, data_dictionary: Dict) -> Any: + def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen) -> Any: """ User sets up the training and test data to fit their desired model here :param data_dictionary: the dictionary constructed by DataHandler to hold @@ -31,6 +31,9 @@ class LightGBMRegressorMultiTarget(BaseRegressionModel): eval_set = (data_dictionary["test_features"], data_dictionary["test_labels"]) sample_weight = data_dictionary["train_weights"] + if self.continual_learning: + logger.warning('Continual learning not supported for MultiTarget models') + model = MultiOutputRegressor(estimator=lgb) model.fit(X=X, y=y, sample_weight=sample_weight) # , eval_set=eval_set) train_score = model.score(X, y) From 8d16dd804dcc0b0cfe94e1b3389186bc23d8e957 Mon Sep 17 00:00:00 2001 From: Wagner Costa Santos Date: Tue, 6 Sep 2022 15:42:47 -0300 Subject: [PATCH 47/96] hyperopt - freqai - docs and refactoring --- docs/freqai.md | 11 +++ freqtrade/configuration/configuration.py | 10 +++ freqtrade/freqai/data_drawer.py | 28 ++----- freqtrade/freqai/freqai_interface.py | 5 +- freqtrade/templates/FreqaiExampleStrategy.py | 86 +------------------- 5 files changed, 29 insertions(+), 111 deletions(-) diff --git a/docs/freqai.md b/docs/freqai.md index 303c2f151..f8cf64d21 100644 --- a/docs/freqai.md +++ b/docs/freqai.md @@ -279,6 +279,8 @@ The FreqAI strategy requires the user to include the following lines of code in Notice how the `populate_any_indicators()` is where the user adds their own features ([more information](#feature-engineering)) and labels ([more information](#setting-classifier-targets)). See a full example at `templates/FreqaiExampleStrategy.py`. +*Important*: The `self.freqai.start()` function cannot be called outside the `populate_indicators()`. + ### Setting the `startup_candle_count` Users need to take care to set the `startup_candle_count` in their strategy the same way they would for any normal Freqtrade strategy (see details [here](strategy-customization.md#strategy-startup-period)). This value is used by Freqtrade to ensure that a sufficient amount of data is provided when calling on the `dataprovider` to avoid any NaNs at the beginning of the first training. Users can easily set this value by identifying the longest period (in candle units) that they pass to their indicator creation functions (e.g. talib functions). In the present example, the user would pass 20 to as this value (since it is the maximum value in their `indicators_periods_candles`). @@ -532,6 +534,15 @@ for each pair, for each backtesting window within the expanded `--timerange`. --- +### Hyperopt + +The [Hyperopt](hyperopt.md) module can be executed with some restrictions: + +- The `--analyze-per-epoch` hyperopt parameter is not compatible with FreqAI. +- It's not possible to hyperopt indicators in `populate_any_indicators()` function. This means that the user cannot optimize model parameters using hyperopt. Apart from this exception, it is possible to optimize all other [spaces](hyperopt.md###runninghyperoptwithsmallersearchspace). +- The [Backtesting](#backtesting) instructions also apply apply to Hyperopt. + + ### Deciding the size of the sliding training window and backtesting duration The user defines the backtesting timerange with the typical `--timerange` parameter in the diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 7c68ac46c..d18b67ff2 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -285,6 +285,7 @@ class Configuration: logger.info('Parameter --stoplosses detected: %s ...', self.args["stoploss_range"]) # Hyperopt section + self._check_hyperopt_analyze_per_epoch_freqai() self._args_to_config(config, argname='hyperopt', logstring='Using Hyperopt class name: {}') @@ -537,3 +538,12 @@ class Configuration: config['pairs'] = load_file(pairs_file) if 'pairs' in config and isinstance(config['pairs'], list): config['pairs'].sort() + + def _check_hyperopt_analyze_per_epoch_freqai(self) -> None: + """ + Helper for block hyperopt with analyze-per-epoch param. + """ + if ("analyze_per_epoch" in self.args and + self.args["analyze_per_epoch"] and "freqaimodel" in self.args): + raise OperationalException('analyze-per-epoch parameter is \ + not allowed with a Freqai strategy.') diff --git a/freqtrade/freqai/data_drawer.py b/freqtrade/freqai/data_drawer.py index dff6b5942..9eeabef8f 100644 --- a/freqtrade/freqai/data_drawer.py +++ b/freqtrade/freqai/data_drawer.py @@ -93,16 +93,6 @@ class FreqaiDataDrawer: "model_filename": "", "trained_timestamp": 0, "priority": 1, "first": True, "data_path": "", "extras": {}} - def __getstate__(self): - """ - Return state values to be pickled. - It's necessary to allow serialization in hyperopt - """ - return ({ - "pair_dict": self.pair_dict, - "pair_dictionary_path": self.pair_dictionary_path - }) - def load_drawer_from_disk(self): """ Locate and load a previously saved data drawer full of all pair model metadata in @@ -165,22 +155,14 @@ class FreqaiDataDrawer: # create a backup shutil.copy(self.historic_predictions_path, self.historic_predictions_bkp_path) - def save_drawer_to_disk(self, live=False): + def save_drawer_to_disk(self): """ Save data drawer full of all pair model metadata in present model folder. """ - if live: - with self.save_lock: - with open(self.pair_dictionary_path, 'w') as fp: - rapidjson.dump( - self.pair_dict, fp, default=self.np_encoder, - number_mode=rapidjson.NM_NATIVE) - else: - # save_lock it's not working with hyperopt + with self.save_lock: with open(self.pair_dictionary_path, 'w') as fp: - rapidjson.dump( - self.pair_dict, fp, default=self.np_encoder, - number_mode=rapidjson.NM_NATIVE) + rapidjson.dump(self.pair_dict, fp, default=self.np_encoder, + number_mode=rapidjson.NM_NATIVE) def save_follower_dict_to_disk(self): """ @@ -455,7 +437,7 @@ class FreqaiDataDrawer: self.model_dictionary[coin] = model self.pair_dict[coin]["model_filename"] = dk.model_filename self.pair_dict[coin]["data_path"] = str(dk.data_path) - self.save_drawer_to_disk(dk.live) + self.save_drawer_to_disk() return diff --git a/freqtrade/freqai/freqai_interface.py b/freqtrade/freqai/freqai_interface.py index 9a05e8383..f631c9126 100644 --- a/freqtrade/freqai/freqai_interface.py +++ b/freqtrade/freqai/freqai_interface.py @@ -92,10 +92,9 @@ class IFreqaiModel(ABC): def __getstate__(self): """ - Return state values to be pickled. - It's necessary to allow serialization in hyperopt + Return an empty state to be pickled in hyperopt """ - return ({"dd": self.dd}) + return ({}) def assert_config(self, config: Dict[str, Any]) -> None: diff --git a/freqtrade/templates/FreqaiExampleStrategy.py b/freqtrade/templates/FreqaiExampleStrategy.py index 0e822a028..78132b06d 100644 --- a/freqtrade/templates/FreqaiExampleStrategy.py +++ b/freqtrade/templates/FreqaiExampleStrategy.py @@ -6,9 +6,7 @@ import talib.abstract as ta from pandas import DataFrame from technical import qtpylib -from freqtrade.exchange import timeframe_to_prev_date -from freqtrade.persistence import Trade -from freqtrade.strategy import DecimalParameter, IntParameter, IStrategy, merge_informative_pair +from freqtrade.strategy import IStrategy, merge_informative_pair logger = logging.getLogger(__name__) @@ -47,11 +45,6 @@ class FreqaiExampleStrategy(IStrategy): startup_candle_count: int = 40 can_short = False - linear_roi_offset = DecimalParameter( - 0.00, 0.02, default=0.005, space="sell", optimize=False, load=True - ) - max_roi_time_long = IntParameter(0, 800, default=400, space="sell", optimize=False, load=True) - def informative_pairs(self): whitelist_pairs = self.dp.current_whitelist() corr_pairs = self.config["freqai"]["feature_parameters"]["include_corr_pairlist"] @@ -226,83 +219,6 @@ class FreqaiExampleStrategy(IStrategy): def get_ticker_indicator(self): return int(self.config["timeframe"][:-1]) - def custom_exit( - self, pair: str, trade: Trade, current_time, current_rate, current_profit, **kwargs - ): - - dataframe, _ = self.dp.get_analyzed_dataframe(pair=pair, timeframe=self.timeframe) - - trade_date = timeframe_to_prev_date(self.config["timeframe"], trade.open_date_utc) - trade_candle = dataframe.loc[(dataframe["date"] == trade_date)] - - if trade_candle.empty: - return None - trade_candle = trade_candle.squeeze() - - follow_mode = self.config.get("freqai", {}).get("follow_mode", False) - - if not follow_mode: - pair_dict = self.freqai.dd.pair_dict - else: - pair_dict = self.freqai.dd.follower_dict - - entry_tag = trade.enter_tag - - if ( - "prediction" + entry_tag not in pair_dict[pair] - or pair_dict[pair]['extras']["prediction" + entry_tag] == 0 - ): - pair_dict[pair]['extras']["prediction" + entry_tag] = abs(trade_candle["&-s_close"]) - if not follow_mode: - self.freqai.dd.save_drawer_to_disk() - else: - self.freqai.dd.save_follower_dict_to_disk() - - roi_price = pair_dict[pair]['extras']["prediction" + entry_tag] - roi_time = self.max_roi_time_long.value - - roi_decay = roi_price * ( - 1 - ((current_time - trade.open_date_utc).seconds) / (roi_time * 60) - ) - if roi_decay < 0: - roi_decay = self.linear_roi_offset.value - else: - roi_decay += self.linear_roi_offset.value - - if current_profit > roi_decay: - return "roi_custom_win" - - if current_profit < -roi_decay: - return "roi_custom_loss" - - def confirm_trade_exit( - self, - pair: str, - trade: Trade, - order_type: str, - amount: float, - rate: float, - time_in_force: str, - exit_reason: str, - current_time, - **kwargs, - ) -> bool: - - entry_tag = trade.enter_tag - follow_mode = self.config.get("freqai", {}).get("follow_mode", False) - if not follow_mode: - pair_dict = self.freqai.dd.pair_dict - else: - pair_dict = self.freqai.dd.follower_dict - - pair_dict[pair]['extras']["prediction" + entry_tag] = 0 - if not follow_mode: - self.freqai.dd.save_drawer_to_disk() - else: - self.freqai.dd.save_follower_dict_to_disk() - - return True - def confirm_trade_entry( self, pair: str, From 5aba5de20f5dea1f61aa7ced19a966744b5c3e30 Mon Sep 17 00:00:00 2001 From: Wagner Costa Santos Date: Tue, 6 Sep 2022 16:17:10 -0300 Subject: [PATCH 48/96] fix link - hyperopt spaces --- docs/freqai.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/freqai.md b/docs/freqai.md index fa58feb07..7aabb0e56 100644 --- a/docs/freqai.md +++ b/docs/freqai.md @@ -539,7 +539,7 @@ for each pair, for each backtesting window within the expanded `--timerange`. The [Hyperopt](hyperopt.md) module can be executed with some restrictions: - The `--analyze-per-epoch` hyperopt parameter is not compatible with FreqAI. -- It's not possible to hyperopt indicators in `populate_any_indicators()` function. This means that the user cannot optimize model parameters using hyperopt. Apart from this exception, it is possible to optimize all other [spaces](hyperopt.md###runninghyperoptwithsmallersearchspace). +- It's not possible to hyperopt indicators in `populate_any_indicators()` function. This means that the user cannot optimize model parameters using hyperopt. Apart from this exception, it is possible to optimize all other [spaces](hyperopt.md#running-hyperopt-with-smaller-search-space). - The [Backtesting](#backtesting) instructions also apply apply to Hyperopt. From 95a33ab2e639d8510c45e731946c5ed2a9189493 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 7 Sep 2022 06:34:03 +0200 Subject: [PATCH 49/96] Add amount_to_contract helper in the exchange --- freqtrade/exchange/exchange.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index c3dca43a8..af072d555 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -446,6 +446,15 @@ class Exchange: contract_size = self.get_contract_size(pair) return contracts_to_amount(num_contracts, contract_size) + def amount_to_contract_precision(self, pair: str, amount: float) -> float: + """ + Helper wrapper around amount_to_contract_precision + """ + contract_size = self.get_contract_size(pair) + + return amount_to_contract_precision(amount, self.get_precision_amount(pair), + self.precisionMode, contract_size) + def set_sandbox(self, api: ccxt.Exchange, exchange_config: dict, name: str) -> None: if exchange_config.get('sandbox'): if api.urls.get('test'): From 83d9f3aeba221bb7a1934a193de66bb4c3887019 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 7 Sep 2022 06:35:58 +0200 Subject: [PATCH 50/96] Add test showing #7365 --- tests/test_integration.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/tests/test_integration.py b/tests/test_integration.py index dd3488f81..77ed822d1 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -485,7 +485,7 @@ def test_dca_exiting(default_conf_usdt, ticker_usdt, fee, mocker, caplog) -> Non assert len(trade.orders) == 1 assert pytest.approx(trade.stake_amount) == 60 assert pytest.approx(trade.amount) == 30.0 - assert log_has_re("Remaining amount of 1.6.* would be too small.", caplog) + assert log_has_re("Remaining amount of 1.6.* would be smaller than the minimum of 10.", caplog) freqtrade.strategy.adjust_trade_position = MagicMock(return_value=-20) @@ -504,9 +504,21 @@ def test_dca_exiting(default_conf_usdt, ticker_usdt, fee, mocker, caplog) -> Non freqtrade.strategy.adjust_trade_position = MagicMock(return_value=-50) freqtrade.process() assert log_has_re("Adjusting amount to trade.amount as it is higher.*", caplog) - assert log_has_re("Remaining amount of 0.0 would be too small.", caplog) + assert log_has_re("Remaining amount of 0.0 would be smaller than the minimum of 10.", caplog) trade = Trade.get_trades().first() assert len(trade.orders) == 2 assert trade.orders[-1].ft_order_side == 'sell' assert pytest.approx(trade.stake_amount) == 40.198 assert trade.is_open + + # use amount that would trunc to 0.0 once selling + mocker.patch("freqtrade.exchange.Exchange.amount_to_contract_precision", + lambda s, p, v: round(v, 1)) + freqtrade.strategy.adjust_trade_position = MagicMock(return_value=-0.01) + freqtrade.process() + trade = Trade.get_trades().first() + assert len(trade.orders) == 2 + assert trade.orders[-1].ft_order_side == 'sell' + assert pytest.approx(trade.stake_amount) == 40.198 + assert trade.is_open + assert log_has_re('Amount to sell is 0.0 due to exchange limits - not selling.', caplog) From 4d69df08dde96d067d9daebbf2db3a61a24dd44a Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 7 Sep 2022 06:43:08 +0200 Subject: [PATCH 51/96] trunc to amount precision before checking valid partial exits closes #7368 --- freqtrade/freqtradebot.py | 11 +++++++++-- freqtrade/optimize/backtesting.py | 6 +++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 1d171ae89..a2f39afd6 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -583,7 +583,9 @@ class FreqtradeBot(LoggingMixin): if stake_amount is not None and stake_amount < 0.0: # We should decrease our position - amount = abs(float(FtPrecise(stake_amount) / FtPrecise(current_exit_rate))) + amount = self.exchange.amount_to_contract_precision( + trade.pair, + abs(float(FtPrecise(stake_amount) / FtPrecise(current_exit_rate)))) if amount > trade.amount: # This is currently ineffective as remaining would become < min tradable # Fixing this would require checking for 0.0 there - @@ -592,9 +594,14 @@ class FreqtradeBot(LoggingMixin): f"Adjusting amount to trade.amount as it is higher. {amount} > {trade.amount}") amount = trade.amount + if amount == 0.0: + logger.info("Amount to sell is 0.0 due to exchange limits - not selling.") + return + remaining = (trade.amount - amount) * current_exit_rate if remaining < min_exit_stake: - logger.info(f'Remaining amount of {remaining} would be too small.') + logger.info(f"Remaining amount of {remaining} would be smaller " + f"than the minimum of {min_exit_stake}.") return self.execute_trade_exit(trade, current_exit_rate, exit_check=ExitCheckTuple( diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 8f6b6b332..97418b72c 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -537,7 +537,11 @@ class Backtesting: return pos_trade if stake_amount is not None and stake_amount < 0.0: - amount = abs(stake_amount) / current_rate + amount = amount_to_contract_precision( + abs(stake_amount) / current_rate, trade.amount_precision, + self.precision_mode, trade.contract_size) + if amount == 0.0: + return trade if amount > trade.amount: # This is currently ineffective as remaining would become < min tradable amount = trade.amount From 3454a52b9523ed567f0c009cc7b3bf8c6100b75f Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 7 Sep 2022 06:55:22 +0200 Subject: [PATCH 52/96] Explicitly test amount_to_contract_precision --- tests/exchange/test_exchange.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 3352019a9..3b903f8ee 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -4456,6 +4456,39 @@ def test__amount_to_contracts( assert result_amount == param_amount +@pytest.mark.parametrize('pair,amount,expected_spot,expected_fut', [ + # Contract size of 0.01 + ('ADA/USDT:USDT', 40, 40, 40), + ('ADA/USDT:USDT', 10.4445555, 10.4, 10.444), + ('LTC/ETH', 30, 30, 30), + ('LTC/USD', 30, 30, 30), + # contract size of 10 + ('ETH/USDT:USDT', 10.111, 10.1, 10), + ('ETH/USDT:USDT', 10.188, 10.1, 10), + ('ETH/USDT:USDT', 10.988, 10.9, 10), +]) +def test_amount_to_contract_precision( + mocker, + default_conf, + pair, + amount, + expected_spot, + expected_fut, +): + api_mock = MagicMock() + default_conf['trading_mode'] = 'spot' + default_conf['margin_mode'] = 'isolated' + exchange = get_patched_exchange(mocker, default_conf, api_mock) + + result_size = exchange.amount_to_contract_precision(pair, amount) + assert result_size == expected_spot + + default_conf['trading_mode'] = 'futures' + exchange = get_patched_exchange(mocker, default_conf, api_mock) + result_size = exchange.amount_to_contract_precision(pair, amount) + assert result_size == expected_fut + + @pytest.mark.parametrize('exchange_name,open_rate,is_short,trading_mode,margin_mode', [ # Bittrex ('bittrex', 2.0, False, 'spot', None), From 972b6991057d00ea1d321403cebf8e2feefa57ff Mon Sep 17 00:00:00 2001 From: Wagner Costa Santos Date: Wed, 7 Sep 2022 11:11:31 -0300 Subject: [PATCH 53/96] hyperopt - freqai - change validation to config_validation --- freqtrade/configuration/config_validation.py | 9 +++++++++ freqtrade/configuration/configuration.py | 10 ---------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/freqtrade/configuration/config_validation.py b/freqtrade/configuration/config_validation.py index ee846e7e6..b37d44d0f 100644 --- a/freqtrade/configuration/config_validation.py +++ b/freqtrade/configuration/config_validation.py @@ -84,6 +84,7 @@ def validate_config_consistency(conf: Dict[str, Any], preliminary: bool = False) _validate_protections(conf) _validate_unlimited_amount(conf) _validate_ask_orderbook(conf) + _validate_freqai_hyperopt(conf) validate_migrated_strategy_settings(conf) # validate configuration before returning @@ -323,6 +324,14 @@ def _validate_pricing_rules(conf: Dict[str, Any]) -> None: del conf['ask_strategy'] +def _validate_freqai_hyperopt(conf: Dict[str, Any]) -> None: + freqaimodel = conf.get('freqaimodel') + analyze_per_epoch = conf.get('analyze_per_epoch', False) + if analyze_per_epoch and freqaimodel is not None: + raise OperationalException( + 'Using analyze-per-epoch parameter is not supported with a FreqAI strategy.') + + def _strategy_settings(conf: Dict[str, Any]) -> None: process_deprecated_setting(conf, None, 'use_sell_signal', None, 'use_exit_signal') diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index d18b67ff2..7c68ac46c 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -285,7 +285,6 @@ class Configuration: logger.info('Parameter --stoplosses detected: %s ...', self.args["stoploss_range"]) # Hyperopt section - self._check_hyperopt_analyze_per_epoch_freqai() self._args_to_config(config, argname='hyperopt', logstring='Using Hyperopt class name: {}') @@ -538,12 +537,3 @@ class Configuration: config['pairs'] = load_file(pairs_file) if 'pairs' in config and isinstance(config['pairs'], list): config['pairs'].sort() - - def _check_hyperopt_analyze_per_epoch_freqai(self) -> None: - """ - Helper for block hyperopt with analyze-per-epoch param. - """ - if ("analyze_per_epoch" in self.args and - self.args["analyze_per_epoch"] and "freqaimodel" in self.args): - raise OperationalException('analyze-per-epoch parameter is \ - not allowed with a Freqai strategy.') From 2c9b7659539ddbf9063f69f4a06429c3790552fc Mon Sep 17 00:00:00 2001 From: Timothy Pogue Date: Wed, 7 Sep 2022 09:35:37 -0600 Subject: [PATCH 54/96] add suffix parameter --- freqtrade/strategy/strategy_helper.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/freqtrade/strategy/strategy_helper.py b/freqtrade/strategy/strategy_helper.py index 43728dc1f..55afbf7a8 100644 --- a/freqtrade/strategy/strategy_helper.py +++ b/freqtrade/strategy/strategy_helper.py @@ -1,3 +1,5 @@ +from typing import Optional + import pandas as pd from freqtrade.exchange import timeframe_to_minutes @@ -6,7 +8,8 @@ from freqtrade.exchange import timeframe_to_minutes def merge_informative_pair(dataframe: pd.DataFrame, informative: pd.DataFrame, timeframe: str, timeframe_inf: str, ffill: bool = True, append_timeframe: bool = True, - date_column: str = 'date') -> pd.DataFrame: + date_column: str = 'date', + suffix: Optional[str] = None) -> pd.DataFrame: """ Correctly merge informative samples to the original dataframe, avoiding lookahead bias. @@ -50,10 +53,17 @@ def merge_informative_pair(dataframe: pd.DataFrame, informative: pd.DataFrame, # Rename columns to be unique date_merge = 'date_merge' - if append_timeframe: + if append_timeframe and not suffix: date_merge = f'date_merge_{timeframe_inf}' informative.columns = [f"{col}_{timeframe_inf}" for col in informative.columns] + elif suffix: + date_merge = f'date_merge_{suffix}' + informative.columns = [f"{col}_{suffix}" for col in informative.columns] + + elif suffix and append_timeframe: + raise ValueError("You can not specify `append_timeframe` as True and a `suffix`.") + # Combine the 2 dataframes # all indicators on the informative sample MUST be calculated before this point if ffill: From 047ded1baa7c7e980b98c8289f689e2f900cf203 Mon Sep 17 00:00:00 2001 From: th0rntwig Date: Wed, 7 Sep 2022 17:47:27 +0200 Subject: [PATCH 55/96] Check for constant columns --- freqtrade/freqai/data_kitchen.py | 41 ++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/freqtrade/freqai/data_kitchen.py b/freqtrade/freqai/data_kitchen.py index 7670cfd45..0a2355c48 100644 --- a/freqtrade/freqai/data_kitchen.py +++ b/freqtrade/freqai/data_kitchen.py @@ -184,7 +184,7 @@ class FreqaiDataKitchen: def filter_features( self, - unfiltered_dataframe: DataFrame, + unfiltered_df: DataFrame, training_feature_list: List, label_list: List = list(), training_filter: bool = True, @@ -195,31 +195,36 @@ class FreqaiDataKitchen: 0s in the prediction dataset. However, prediction dataset do_predict will reflect any row that had a NaN and will shield user from that prediction. :params: - :unfiltered_dataframe: the full dataframe for the present training period + :unfiltered_df: the full dataframe for the present training period :training_feature_list: list, the training feature list constructed by self.build_feature_list() according to user specified parameters in the configuration file. :labels: the labels for the dataset :training_filter: boolean which lets the function know if it is training data or prediction data to be filtered. :returns: - :filtered_dataframe: dataframe cleaned of NaNs and only containing the user + :filtered_df: dataframe cleaned of NaNs and only containing the user requested feature set. :labels: labels cleaned of NaNs. """ - filtered_dataframe = unfiltered_dataframe.filter(training_feature_list, axis=1) - filtered_dataframe = filtered_dataframe.replace([np.inf, -np.inf], np.nan) + filtered_df = unfiltered_df.filter(training_feature_list, axis=1) + filtered_df = filtered_df.replace([np.inf, -np.inf], np.nan) - drop_index = pd.isnull(filtered_dataframe).any(1) # get the rows that have NaNs, + const_cols = filtered_df[:, filtered_df.nunique(axis=0) == len(filtered_df.index)].columns + if const_cols: + filtered_df = filtered_df.filter(filtered_df.columns.difference(const_cols)) + logger.warning(f"Removed features {const_cols} with constant values.") + + drop_index = pd.isnull(filtered_df).any(1) # get the rows that have NaNs, drop_index = drop_index.replace(True, 1).replace(False, 0) # pep8 requirement. if (training_filter): # we don't care about total row number (total no. datapoints) in training, we only care # about removing any row with NaNs # if labels has multiple columns (user wants to train multiple modelEs), we detect here - labels = unfiltered_dataframe.filter(label_list, axis=1) + labels = unfiltered_df.filter(label_list, axis=1) drop_index_labels = pd.isnull(labels).any(1) drop_index_labels = drop_index_labels.replace(True, 1).replace(False, 0) - dates = unfiltered_dataframe['date'] - filtered_dataframe = filtered_dataframe[ + dates = unfiltered_df['date'] + filtered_df = filtered_df[ (drop_index == 0) & (drop_index_labels == 0) ] # dropping values labels = labels[ @@ -229,13 +234,13 @@ class FreqaiDataKitchen: (drop_index == 0) & (drop_index_labels == 0) ] logger.info( - f"dropped {len(unfiltered_dataframe) - len(filtered_dataframe)} training points" - f" due to NaNs in populated dataset {len(unfiltered_dataframe)}." + f"dropped {len(unfiltered_df) - len(filtered_df)} training points" + f" due to NaNs in populated dataset {len(unfiltered_df)}." ) - if (1 - len(filtered_dataframe) / len(unfiltered_dataframe)) > 0.1 and self.live: - worst_indicator = str(unfiltered_dataframe.count().idxmin()) + if (1 - len(filtered_df) / len(unfiltered_df)) > 0.1 and self.live: + worst_indicator = str(unfiltered_df.count().idxmin()) logger.warning( - f" {(1 - len(filtered_dataframe)/len(unfiltered_dataframe)) * 100:.0f} percent " + f" {(1 - len(filtered_df)/len(unfiltered_df)) * 100:.0f} percent " " of training data dropped due to NaNs, model may perform inconsistent " f"with expectations. Verify {worst_indicator}" ) @@ -244,9 +249,9 @@ class FreqaiDataKitchen: else: # we are backtesting so we need to preserve row number to send back to strategy, # so now we use do_predict to avoid any prediction based on a NaN - drop_index = pd.isnull(filtered_dataframe).any(1) + drop_index = pd.isnull(filtered_df).any(1) self.data["filter_drop_index_prediction"] = drop_index - filtered_dataframe.fillna(0, inplace=True) + filtered_df.fillna(0, inplace=True) # replacing all NaNs with zeros to avoid issues in 'prediction', but any prediction # that was based on a single NaN is ultimately protected from buys with do_predict drop_index = ~drop_index @@ -255,11 +260,11 @@ class FreqaiDataKitchen: logger.info( "dropped %s of %s prediction data points due to NaNs.", len(self.do_predict) - self.do_predict.sum(), - len(filtered_dataframe), + len(filtered_df), ) labels = [] - return filtered_dataframe, labels + return filtered_df, labels def build_data_dictionary( self, From cdc72bf8cab4bf78b2da460690a3b71ab076bff4 Mon Sep 17 00:00:00 2001 From: th0rntwig Date: Wed, 7 Sep 2022 18:14:13 +0200 Subject: [PATCH 56/96] Correct indexing --- freqtrade/freqai/data_kitchen.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/freqtrade/freqai/data_kitchen.py b/freqtrade/freqai/data_kitchen.py index 0a2355c48..790b3c078 100644 --- a/freqtrade/freqai/data_kitchen.py +++ b/freqtrade/freqai/data_kitchen.py @@ -209,7 +209,7 @@ class FreqaiDataKitchen: filtered_df = unfiltered_df.filter(training_feature_list, axis=1) filtered_df = filtered_df.replace([np.inf, -np.inf], np.nan) - const_cols = filtered_df[:, filtered_df.nunique(axis=0) == len(filtered_df.index)].columns + const_cols = list((filtered_df.nunique() == 1).loc[lambda x: x].index) if const_cols: filtered_df = filtered_df.filter(filtered_df.columns.difference(const_cols)) logger.warning(f"Removed features {const_cols} with constant values.") @@ -1205,7 +1205,6 @@ class FreqaiDataKitchen: def save_backtesting_prediction( self, append_df: DataFrame ) -> None: - """ Save prediction dataframe from backtesting to h5 file format :param append_df: dataframe for backtesting period @@ -1219,7 +1218,6 @@ class FreqaiDataKitchen: def get_backtesting_prediction( self ) -> DataFrame: - """ Get prediction dataframe from h5 file format """ From 322f00e3e8c7c183e350e84e4c740e0d34130c68 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 7 Sep 2022 18:19:25 +0200 Subject: [PATCH 57/96] Fix empty entry message if order doesn't fill immediately closes #7368 --- freqtrade/freqtradebot.py | 2 +- tests/test_freqtradebot.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index a2f39afd6..ec32cae0e 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -919,7 +919,7 @@ class FreqtradeBot(LoggingMixin): 'stake_amount': trade.stake_amount, 'stake_currency': self.config['stake_currency'], 'fiat_currency': self.config.get('fiat_display_currency', None), - 'amount': order.safe_amount_after_fee, + 'amount': order.safe_amount_after_fee if fill else order.amount, 'open_date': trade.open_date or datetime.utcnow(), 'current_rate': current_rate, 'sub_trade': sub_trade, diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index aff0504b3..30b0b75b6 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -3655,6 +3655,7 @@ def test_may_execute_trade_exit_after_stoploss_on_exchange_hit( assert trade.exit_reason == ExitType.STOPLOSS_ON_EXCHANGE.value assert rpc_mock.call_count == 3 assert rpc_mock.call_args_list[0][0][0]['type'] == RPCMessageType.ENTRY + assert rpc_mock.call_args_list[0][0][0]['amount'] > 20 assert rpc_mock.call_args_list[1][0][0]['type'] == RPCMessageType.ENTRY_FILL assert rpc_mock.call_args_list[2][0][0]['type'] == RPCMessageType.EXIT_FILL From ea7bdac9edc7b3f5c5e039e509b8d368f7045a3e Mon Sep 17 00:00:00 2001 From: robcaulk Date: Wed, 7 Sep 2022 18:45:16 +0200 Subject: [PATCH 58/96] ensure inlier metric can be combined with other cleaning methods --- freqtrade/freqai/freqai_interface.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/freqtrade/freqai/freqai_interface.py b/freqtrade/freqai/freqai_interface.py index 5ccc9d1b2..32e42e115 100644 --- a/freqtrade/freqai/freqai_interface.py +++ b/freqtrade/freqai/freqai_interface.py @@ -427,6 +427,11 @@ class IFreqaiModel(ABC): ft_params = self.freqai_info["feature_parameters"] + if ft_params.get('inlier_metric_window', 0): + dk.compute_inlier_metric(set_='train') + if self.freqai_info["data_split_parameters"]["test_size"] > 0: + dk.compute_inlier_metric(set_='test') + if ft_params.get( "principal_component_analysis", False ): @@ -446,11 +451,6 @@ class IFreqaiModel(ABC): dk.use_DBSCAN_to_remove_outliers(predict=False, eps=eps) self.dd.old_DBSCAN_eps[dk.pair] = dk.data['DBSCAN_eps'] - if ft_params.get('inlier_metric_window', 0): - dk.compute_inlier_metric(set_='train') - if self.freqai_info["data_split_parameters"]["test_size"] > 0: - dk.compute_inlier_metric(set_='test') - if self.freqai_info["feature_parameters"].get('noise_standard_deviation', 0): dk.add_noise_to_training_features() From 48cadbf933e829c8335a3ef593aa324b7d08a659 Mon Sep 17 00:00:00 2001 From: Timothy Pogue Date: Wed, 7 Sep 2022 09:52:22 -0600 Subject: [PATCH 59/96] remove duplicate line, change window to timeperiod --- freqtrade/templates/FreqaiExampleStrategy.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/freqtrade/templates/FreqaiExampleStrategy.py b/freqtrade/templates/FreqaiExampleStrategy.py index 0e822a028..b172ab805 100644 --- a/freqtrade/templates/FreqaiExampleStrategy.py +++ b/freqtrade/templates/FreqaiExampleStrategy.py @@ -92,12 +92,10 @@ class FreqaiExampleStrategy(IStrategy): t = int(t) informative[f"%-{coin}rsi-period_{t}"] = ta.RSI(informative, timeperiod=t) informative[f"%-{coin}mfi-period_{t}"] = ta.MFI(informative, timeperiod=t) - informative[f"%-{coin}adx-period_{t}"] = ta.ADX(informative, window=t) + informative[f"%-{coin}adx-period_{t}"] = ta.ADX(informative, timeperiod=t) informative[f"%-{coin}sma-period_{t}"] = ta.SMA(informative, timeperiod=t) informative[f"%-{coin}ema-period_{t}"] = ta.EMA(informative, timeperiod=t) - informative[f"%-{coin}mfi-period_{t}"] = ta.MFI(informative, timeperiod=t) - bollinger = qtpylib.bollinger_bands( qtpylib.typical_price(informative), window=t, stds=2.2 ) From 5d338e697c6df2d7d3d5109fb7ce29309f6dcdf0 Mon Sep 17 00:00:00 2001 From: Timothy Pogue Date: Wed, 7 Sep 2022 09:59:46 -0600 Subject: [PATCH 60/96] change window to timeperiod in hybrid --- freqtrade/templates/FreqaiHybridExampleStrategy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/templates/FreqaiHybridExampleStrategy.py b/freqtrade/templates/FreqaiHybridExampleStrategy.py index 5d1e149dd..286ff012f 100644 --- a/freqtrade/templates/FreqaiHybridExampleStrategy.py +++ b/freqtrade/templates/FreqaiHybridExampleStrategy.py @@ -135,7 +135,7 @@ class FreqaiExampleHybridStrategy(IStrategy): t = int(t) informative[f"%-{coin}rsi-period_{t}"] = ta.RSI(informative, timeperiod=t) informative[f"%-{coin}mfi-period_{t}"] = ta.MFI(informative, timeperiod=t) - informative[f"%-{coin}adx-period_{t}"] = ta.ADX(informative, window=t) + informative[f"%-{coin}adx-period_{t}"] = ta.ADX(informative, timeperiod=t) informative[f"%-{coin}sma-period_{t}"] = ta.SMA(informative, timeperiod=t) informative[f"%-{coin}ema-period_{t}"] = ta.EMA(informative, timeperiod=t) informative[f"%-{coin}roc-period_{t}"] = ta.ROC(informative, timeperiod=t) From 4c9ac6b7c0959a7d596279a1388145f8a90bd8da Mon Sep 17 00:00:00 2001 From: robcaulk Date: Wed, 7 Sep 2022 18:58:55 +0200 Subject: [PATCH 61/96] add kwargs, reduce duplicated code --- freqtrade/freqai/freqai_interface.py | 15 ++++++++++++--- .../prediction_models/CatboostClassifier.py | 8 +++----- .../freqai/prediction_models/CatboostRegressor.py | 10 +++------- .../CatboostRegressorMultiTarget.py | 3 ++- .../prediction_models/LightGBMClassifier.py | 10 ++++------ .../freqai/prediction_models/LightGBMRegressor.py | 10 ++++------ .../LightGBMRegressorMultiTarget.py | 5 +++-- 7 files changed, 31 insertions(+), 30 deletions(-) diff --git a/freqtrade/freqai/freqai_interface.py b/freqtrade/freqai/freqai_interface.py index b6f3d8ebc..101df88ec 100644 --- a/freqtrade/freqai/freqai_interface.py +++ b/freqtrade/freqai/freqai_interface.py @@ -661,11 +661,20 @@ class IFreqaiModel(ABC): self.train_time = 0 return + def get_init_model(self, pair: str) -> Any: + if pair not in self.dd.model_dictionary or not self.continual_learning: + init_model = None + else: + init_model = self.dd.model_dictionary[pair] + + return init_model + # Following methods which are overridden by user made prediction models. # See freqai/prediction_models/CatboostPredictionModel.py for an example. @abstractmethod - def train(self, unfiltered_dataframe: DataFrame, pair: str, dk: FreqaiDataKitchen) -> Any: + def train(self, unfiltered_dataframe: DataFrame, pair: str, + dk: FreqaiDataKitchen, **kwargs) -> Any: """ Filter the training data and train a model to it. Train makes heavy use of the datahandler for storing, saving, loading, and analyzing the data. @@ -675,7 +684,7 @@ class IFreqaiModel(ABC): """ @abstractmethod - def fit(self, data_dictionary: Dict[str, Any], dk: FreqaiDataKitchen) -> Any: + def fit(self, data_dictionary: Dict[str, Any], dk: FreqaiDataKitchen, **kwargs) -> Any: """ Most regressors use the same function names and arguments e.g. user can drop in LGBMRegressor in place of CatBoostRegressor and all data @@ -688,7 +697,7 @@ class IFreqaiModel(ABC): @abstractmethod def predict( - self, dataframe: DataFrame, dk: FreqaiDataKitchen, first: bool = True + self, dataframe: DataFrame, dk: FreqaiDataKitchen, first: bool = True, **kwargs ) -> Tuple[DataFrame, NDArray[np.int_]]: """ Filter the prediction features data and predict with it. diff --git a/freqtrade/freqai/prediction_models/CatboostClassifier.py b/freqtrade/freqai/prediction_models/CatboostClassifier.py index 13395879a..cd7afd392 100644 --- a/freqtrade/freqai/prediction_models/CatboostClassifier.py +++ b/freqtrade/freqai/prediction_models/CatboostClassifier.py @@ -2,6 +2,7 @@ import logging from typing import Any, Dict from catboost import CatBoostClassifier, Pool + from freqtrade.freqai.data_kitchen import FreqaiDataKitchen from freqtrade.freqai.prediction_models.BaseClassifierModel import BaseClassifierModel @@ -16,7 +17,7 @@ class CatboostClassifier(BaseClassifierModel): has its own DataHandler where data is held, saved, loaded, and managed. """ - def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen) -> Any: + def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ User sets up the training and test data to fit their desired model here :params: @@ -36,10 +37,7 @@ class CatboostClassifier(BaseClassifierModel): **self.model_training_parameters, ) - if dk.pair not in self.dd.model_dictionary or not self.continual_learning: - init_model = None - else: - init_model = self.dd.model_dictionary[dk.pair] + init_model = self.get_init_model(dk.pair) cbr.fit(train_data, init_model=init_model) diff --git a/freqtrade/freqai/prediction_models/CatboostRegressor.py b/freqtrade/freqai/prediction_models/CatboostRegressor.py index 0b8bc162b..1ce31b628 100644 --- a/freqtrade/freqai/prediction_models/CatboostRegressor.py +++ b/freqtrade/freqai/prediction_models/CatboostRegressor.py @@ -1,10 +1,9 @@ -import gc import logging from typing import Any, Dict from catboost import CatBoostRegressor, Pool -from freqtrade.freqai.data_kitchen import FreqaiDataKitchen +from freqtrade.freqai.data_kitchen import FreqaiDataKitchen from freqtrade.freqai.prediction_models.BaseRegressionModel import BaseRegressionModel @@ -18,7 +17,7 @@ class CatboostRegressor(BaseRegressionModel): has its own DataHandler where data is held, saved, loaded, and managed. """ - def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen) -> Any: + def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ User sets up the training and test data to fit their desired model here :param data_dictionary: the dictionary constructed by DataHandler to hold @@ -39,10 +38,7 @@ class CatboostRegressor(BaseRegressionModel): weight=data_dictionary["test_weights"], ) - if dk.pair not in self.dd.model_dictionary or not self.continual_learning: - init_model = None - else: - init_model = self.dd.model_dictionary[dk.pair] + init_model = self.get_init_model(dk.pair) model = CatBoostRegressor( allow_writing_files=False, diff --git a/freqtrade/freqai/prediction_models/CatboostRegressorMultiTarget.py b/freqtrade/freqai/prediction_models/CatboostRegressorMultiTarget.py index 9ed61488c..bc52bfdd9 100644 --- a/freqtrade/freqai/prediction_models/CatboostRegressorMultiTarget.py +++ b/freqtrade/freqai/prediction_models/CatboostRegressorMultiTarget.py @@ -3,6 +3,7 @@ from typing import Any, Dict from catboost import CatBoostRegressor # , Pool from sklearn.multioutput import MultiOutputRegressor + from freqtrade.freqai.data_kitchen import FreqaiDataKitchen from freqtrade.freqai.prediction_models.BaseRegressionModel import BaseRegressionModel @@ -17,7 +18,7 @@ class CatboostRegressorMultiTarget(BaseRegressionModel): has its own DataHandler where data is held, saved, loaded, and managed. """ - def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen) -> Any: + def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ User sets up the training and test data to fit their desired model here :param data_dictionary: the dictionary constructed by DataHandler to hold diff --git a/freqtrade/freqai/prediction_models/LightGBMClassifier.py b/freqtrade/freqai/prediction_models/LightGBMClassifier.py index 0023a9f69..69867eae3 100644 --- a/freqtrade/freqai/prediction_models/LightGBMClassifier.py +++ b/freqtrade/freqai/prediction_models/LightGBMClassifier.py @@ -3,8 +3,9 @@ from typing import Any, Dict from lightgbm import LGBMClassifier -from freqtrade.freqai.prediction_models.BaseClassifierModel import BaseClassifierModel from freqtrade.freqai.data_kitchen import FreqaiDataKitchen +from freqtrade.freqai.prediction_models.BaseClassifierModel import BaseClassifierModel + logger = logging.getLogger(__name__) @@ -16,7 +17,7 @@ class LightGBMClassifier(BaseClassifierModel): has its own DataHandler where data is held, saved, loaded, and managed. """ - def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen) -> Any: + def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ User sets up the training and test data to fit their desired model here :params: @@ -35,10 +36,7 @@ class LightGBMClassifier(BaseClassifierModel): y = data_dictionary["train_labels"].to_numpy()[:, 0] train_weights = data_dictionary["train_weights"] - if dk.pair not in self.dd.model_dictionary or not self.continual_learning: - init_model = None - else: - init_model = self.dd.model_dictionary[dk.pair] + init_model = self.get_init_model(dk.pair) model = LGBMClassifier(**self.model_training_parameters) diff --git a/freqtrade/freqai/prediction_models/LightGBMRegressor.py b/freqtrade/freqai/prediction_models/LightGBMRegressor.py index 81f0e6d22..99e9ff887 100644 --- a/freqtrade/freqai/prediction_models/LightGBMRegressor.py +++ b/freqtrade/freqai/prediction_models/LightGBMRegressor.py @@ -3,8 +3,9 @@ from typing import Any, Dict from lightgbm import LGBMRegressor -from freqtrade.freqai.prediction_models.BaseRegressionModel import BaseRegressionModel from freqtrade.freqai.data_kitchen import FreqaiDataKitchen +from freqtrade.freqai.prediction_models.BaseRegressionModel import BaseRegressionModel + logger = logging.getLogger(__name__) @@ -16,7 +17,7 @@ class LightGBMRegressor(BaseRegressionModel): has its own DataHandler where data is held, saved, loaded, and managed. """ - def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen) -> Any: + def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ Most regressors use the same function names and arguments e.g. user can drop in LGBMRegressor in place of CatBoostRegressor and all data @@ -35,10 +36,7 @@ class LightGBMRegressor(BaseRegressionModel): y = data_dictionary["train_labels"] train_weights = data_dictionary["train_weights"] - if dk.pair not in self.dd.model_dictionary or not self.continual_learning: - init_model = None - else: - init_model = self.dd.model_dictionary[dk.pair] + init_model = self.get_init_model(dk.pair) model = LGBMRegressor(**self.model_training_parameters) diff --git a/freqtrade/freqai/prediction_models/LightGBMRegressorMultiTarget.py b/freqtrade/freqai/prediction_models/LightGBMRegressorMultiTarget.py index 2b25493e0..c34680dbe 100644 --- a/freqtrade/freqai/prediction_models/LightGBMRegressorMultiTarget.py +++ b/freqtrade/freqai/prediction_models/LightGBMRegressorMultiTarget.py @@ -4,8 +4,9 @@ from typing import Any, Dict from lightgbm import LGBMRegressor from sklearn.multioutput import MultiOutputRegressor -from freqtrade.freqai.prediction_models.BaseRegressionModel import BaseRegressionModel from freqtrade.freqai.data_kitchen import FreqaiDataKitchen +from freqtrade.freqai.prediction_models.BaseRegressionModel import BaseRegressionModel + logger = logging.getLogger(__name__) @@ -17,7 +18,7 @@ class LightGBMRegressorMultiTarget(BaseRegressionModel): has its own DataHandler where data is held, saved, loaded, and managed. """ - def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen) -> Any: + def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ User sets up the training and test data to fit their desired model here :param data_dictionary: the dictionary constructed by DataHandler to hold From e51d352777d9e06518da0f28c974dceabf4afd0c Mon Sep 17 00:00:00 2001 From: robcaulk Date: Wed, 7 Sep 2022 19:11:54 +0200 Subject: [PATCH 62/96] ensure pca is handling same DF as inlier --- freqtrade/freqai/freqai_interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqai/freqai_interface.py b/freqtrade/freqai/freqai_interface.py index 32e42e115..c5ac17a3a 100644 --- a/freqtrade/freqai/freqai_interface.py +++ b/freqtrade/freqai/freqai_interface.py @@ -467,7 +467,7 @@ class IFreqaiModel(ABC): if ft_params.get( "principal_component_analysis", False ): - dk.pca_transform(dataframe) + dk.pca_transform(self.dk.data_dictionary['prediction_features']) if ft_params.get("use_SVM_to_remove_outliers", False): dk.use_SVM_to_remove_outliers(predict=True) From a9fd12b816d5a36a8233f508dec92d3f4bb4ab40 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 7 Sep 2022 18:28:04 +0200 Subject: [PATCH 63/96] Allow more dynamic timeframes by disabling "choice" part of #7366 --- freqtrade/commands/cli_options.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index 3d094da36..4240a8014 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -455,8 +455,6 @@ AVAILABLE_CLI_OPTIONS = { '-t', '--timeframes', help='Specify which tickers to download. Space-separated list. ' 'Default: `1m 5m`.', - choices=['1m', '3m', '5m', '15m', '30m', '1h', '2h', '4h', - '6h', '8h', '12h', '1d', '3d', '1w', '2w', '1M', '1y'], default=['1m', '5m'], nargs='+', ), From f8e7ed5d7d8f05cd470517a630431725cff48280 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 7 Sep 2022 18:28:14 +0200 Subject: [PATCH 64/96] Reduce shutdown verbosity of exchange --- freqtrade/exchange/exchange.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index af072d555..569dcad9b 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -205,7 +205,7 @@ class Exchange: logger.debug("Exchange object destroyed, closing async loop") if (self._api_async and inspect.iscoroutinefunction(self._api_async.close) and self._api_async.session): - logger.info("Closing async ccxt session.") + logger.debug("Closing async ccxt session.") self.loop.run_until_complete(self._api_async.close()) def validate_config(self, config): From bf3ee51167f183739b0a71677126b9c18d733560 Mon Sep 17 00:00:00 2001 From: Wagner Costa Santos Date: Wed, 7 Sep 2022 14:42:05 -0300 Subject: [PATCH 65/96] validate freqai hyperopt with freqai enabled param --- freqtrade/configuration/config_validation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/configuration/config_validation.py b/freqtrade/configuration/config_validation.py index b37d44d0f..8d9112bef 100644 --- a/freqtrade/configuration/config_validation.py +++ b/freqtrade/configuration/config_validation.py @@ -325,9 +325,9 @@ def _validate_pricing_rules(conf: Dict[str, Any]) -> None: def _validate_freqai_hyperopt(conf: Dict[str, Any]) -> None: - freqaimodel = conf.get('freqaimodel') + freqai_enabled = conf.get('freqai', {}).get('enabled', False) analyze_per_epoch = conf.get('analyze_per_epoch', False) - if analyze_per_epoch and freqaimodel is not None: + if analyze_per_epoch and freqai_enabled: raise OperationalException( 'Using analyze-per-epoch parameter is not supported with a FreqAI strategy.') From 1ef1fc269e2b682a2d8053340e8349436a132269 Mon Sep 17 00:00:00 2001 From: Timothy Pogue Date: Wed, 7 Sep 2022 15:26:38 -0600 Subject: [PATCH 66/96] docstring and tests --- freqtrade/strategy/strategy_helper.py | 4 +++- tests/strategy/test_strategy_helpers.py | 23 +++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/freqtrade/strategy/strategy_helper.py b/freqtrade/strategy/strategy_helper.py index 55afbf7a8..53f625001 100644 --- a/freqtrade/strategy/strategy_helper.py +++ b/freqtrade/strategy/strategy_helper.py @@ -31,6 +31,8 @@ def merge_informative_pair(dataframe: pd.DataFrame, informative: pd.DataFrame, :param ffill: Forwardfill missing values - optional but usually required :param append_timeframe: Rename columns by appending timeframe. :param date_column: A custom date column name. + :param suffix: A string suffix to add at the end of the informative columns. If specified, + append_timeframe must be false. :return: Merged dataframe :raise: ValueError if the secondary timeframe is shorter than the dataframe timeframe """ @@ -57,7 +59,7 @@ def merge_informative_pair(dataframe: pd.DataFrame, informative: pd.DataFrame, date_merge = f'date_merge_{timeframe_inf}' informative.columns = [f"{col}_{timeframe_inf}" for col in informative.columns] - elif suffix: + elif suffix and not append_timeframe: date_merge = f'date_merge_{suffix}' informative.columns = [f"{col}_{suffix}" for col in informative.columns] diff --git a/tests/strategy/test_strategy_helpers.py b/tests/strategy/test_strategy_helpers.py index a7c2da26a..8cb990e87 100644 --- a/tests/strategy/test_strategy_helpers.py +++ b/tests/strategy/test_strategy_helpers.py @@ -117,6 +117,29 @@ def test_merge_informative_pair_lower(): merge_informative_pair(data, informative, '1h', '15m', ffill=True) +def test_merge_informative_pair_suffix(): + data = generate_test_data('15m', 20) + informative = generate_test_data('1h', 20) + + result = merge_informative_pair(data, informative, '15m', '1h', + append_timeframe=False, suffix="suf") + + assert 'date' in result.columns + assert result['date'].equals(data['date']) + assert 'date_suf' in result.columns + + assert 'open_suf' in result.columns + assert 'open_1h' not in result.columns + + +def test_merge_informative_pair_suffix_append_timeframe(): + data = generate_test_data('15m', 20) + informative = generate_test_data('1h', 20) + + with pytest.raises(ValueError, match=r"You can not specify `append_timeframe` .*"): + merge_informative_pair(data, informative, '15m', '1h', suffix="suf") + + def test_stoploss_from_open(): open_price_ranges = [ [0.01, 1.00, 30], From f4f2884a66c97c05e6871b1ecf2f12b1bc0ec460 Mon Sep 17 00:00:00 2001 From: Wagner Costa Santos Date: Wed, 7 Sep 2022 18:52:58 -0300 Subject: [PATCH 67/96] Fix freqai backtesting time range issue --- freqtrade/freqai/data_kitchen.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/freqtrade/freqai/data_kitchen.py b/freqtrade/freqai/data_kitchen.py index 7670cfd45..01b2f1f3b 100644 --- a/freqtrade/freqai/data_kitchen.py +++ b/freqtrade/freqai/data_kitchen.py @@ -461,6 +461,20 @@ class FreqaiDataKitchen: return df + def remove_training_from_backtesting( + self + ) -> DataFrame: + """ + Function which takes the backtesting time range and + remove training data from dataframe + """ + tr = self.config["timerange"] + backtesting_timerange = TimeRange.parse_timerange(tr) + start = datetime.fromtimestamp(backtesting_timerange.startts, tz=timezone.utc) + df = self.return_dataframe + df = df.loc[df["date"] >= start, :] + return df + def principal_component_analysis(self) -> None: """ Performs Principal Component Analysis on the data for dimensionality reduction @@ -954,6 +968,7 @@ class FreqaiDataKitchen: to_keep = [col for col in dataframe.columns if not col.startswith("&")] self.return_dataframe = pd.concat([dataframe[to_keep], self.full_df], axis=1) + self.return_dataframe = self.remove_training_from_backtesting() self.full_df = DataFrame() return From f3417a869069bdb01da844017b00ebf30ee6f208 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 8 Sep 2022 06:59:14 +0200 Subject: [PATCH 68/96] Revert condition sequence to simplify conditions --- freqtrade/strategy/strategy_helper.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/freqtrade/strategy/strategy_helper.py b/freqtrade/strategy/strategy_helper.py index 53f625001..aa753a829 100644 --- a/freqtrade/strategy/strategy_helper.py +++ b/freqtrade/strategy/strategy_helper.py @@ -55,17 +55,16 @@ def merge_informative_pair(dataframe: pd.DataFrame, informative: pd.DataFrame, # Rename columns to be unique date_merge = 'date_merge' - if append_timeframe and not suffix: + if suffix and append_timeframe: + raise ValueError("You can not specify `append_timeframe` as True and a `suffix`.") + elif append_timeframe: date_merge = f'date_merge_{timeframe_inf}' informative.columns = [f"{col}_{timeframe_inf}" for col in informative.columns] - elif suffix and not append_timeframe: + elif suffix: date_merge = f'date_merge_{suffix}' informative.columns = [f"{col}_{suffix}" for col in informative.columns] - elif suffix and append_timeframe: - raise ValueError("You can not specify `append_timeframe` as True and a `suffix`.") - # Combine the 2 dataframes # all indicators on the informative sample MUST be calculated before this point if ffill: From 4e15611b055a7afb5dfc00fd687967fa52bca1bf Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 8 Sep 2022 07:18:38 +0200 Subject: [PATCH 69/96] Don't crash in case of funding fee fetch error --- freqtrade/exchange/exchange.py | 11 +++++-- freqtrade/freqtradebot.py | 57 ++++++++++++++++++++-------------- 2 files changed, 42 insertions(+), 26 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 569dcad9b..be5af91db 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -2509,8 +2509,13 @@ class Exchange: cache=False, drop_incomplete=False, ) - funding_rates = candle_histories[funding_comb] - mark_rates = candle_histories[mark_comb] + try: + # we can't assume we always get histories - for example during exchange downtimes + funding_rates = candle_histories[funding_comb] + mark_rates = candle_histories[mark_comb] + except KeyError: + raise ExchangeError("Could not find funding rates") from None + funding_mark_rates = self.combine_funding_and_mark( funding_rates=funding_rates, mark_rates=mark_rates) @@ -2590,6 +2595,8 @@ class Exchange: :param is_short: trade direction :param amount: Trade amount :param open_date: Open date of the trade + :return: funding fee since open_date + :raies: ExchangeError if something goes wrong. """ if self.trading_mode == TradingMode.FUTURES: if self._config['dry_run']: diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index ec32cae0e..61c323ed3 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -281,14 +281,17 @@ class FreqtradeBot(LoggingMixin): def update_funding_fees(self): if self.trading_mode == TradingMode.FUTURES: trades = Trade.get_open_trades() - for trade in trades: - funding_fees = self.exchange.get_funding_fees( - pair=trade.pair, - amount=trade.amount, - is_short=trade.is_short, - open_date=trade.date_last_filled_utc - ) - trade.funding_fees = funding_fees + try: + for trade in trades: + funding_fees = self.exchange.get_funding_fees( + pair=trade.pair, + amount=trade.amount, + is_short=trade.is_short, + open_date=trade.date_last_filled_utc + ) + trade.funding_fees = funding_fees + except ExchangeError: + logger.warning("Could not update funding fees for open trades.") else: return 0.0 @@ -671,14 +674,12 @@ class FreqtradeBot(LoggingMixin): if not stake_amount: return False - if pos_adjust: - logger.info(f"Position adjust: about to create a new order for {pair} with stake: " - f"{stake_amount} for {trade}") - else: - logger.info( - f"{name} signal found: about create a new trade for {pair} with stake_amount: " - f"{stake_amount} ...") - + msg = (f"Position adjust: about to create a new order for {pair} with stake: " + f"{stake_amount} for {trade}" if pos_adjust + else + f"{name} signal found: about create a new trade for {pair} with stake_amount: " + f"{stake_amount} ...") + logger.info(msg) amount = (stake_amount / enter_limit_requested) * leverage order_type = ordertype or self.strategy.order_types['entry'] @@ -741,8 +742,12 @@ class FreqtradeBot(LoggingMixin): # This is a new trade if trade is None: - funding_fees = self.exchange.get_funding_fees( - pair=pair, amount=amount, is_short=is_short, open_date=open_date) + try: + funding_fees = self.exchange.get_funding_fees( + pair=pair, amount=amount, is_short=is_short, open_date=open_date) + except ExchangeError: + logger.warning("Could not update funding fee.") + trade = Trade( pair=pair, base_currency=base_currency, @@ -1493,12 +1498,16 @@ class FreqtradeBot(LoggingMixin): :param exit_check: CheckTuple with signal and reason :return: True if it succeeds False """ - trade.funding_fees = self.exchange.get_funding_fees( - pair=trade.pair, - amount=trade.amount, - is_short=trade.is_short, - open_date=trade.date_last_filled_utc, - ) + try: + trade.funding_fees = self.exchange.get_funding_fees( + pair=trade.pair, + amount=trade.amount, + is_short=trade.is_short, + open_date=trade.date_last_filled_utc, + ) + except ExchangeError: + logger.warning("Could not update funding fee.") + exit_type = 'exit' exit_reason = exit_tag or exit_check.exit_reason if exit_check.exit_type in ( From 39b6cadd14e8253541c2836daed435059d20670d Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 8 Sep 2022 07:24:57 +0200 Subject: [PATCH 70/96] Test keyerror case for funding_Fee calculation --- freqtrade/exchange/exchange.py | 2 +- tests/exchange/test_exchange.py | 18 +++++++++++++----- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index be5af91db..33a56c530 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -2514,7 +2514,7 @@ class Exchange: funding_rates = candle_histories[funding_comb] mark_rates = candle_histories[mark_comb] except KeyError: - raise ExchangeError("Could not find funding rates") from None + raise ExchangeError("Could not find funding rates.") from None funding_mark_rates = self.combine_funding_and_mark( funding_rates=funding_rates, mark_rates=mark_rates) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 3b903f8ee..71690ecdf 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -11,8 +11,9 @@ import pytest from pandas import DataFrame from freqtrade.enums import CandleType, MarginMode, TradingMode -from freqtrade.exceptions import (DDosProtection, DependencyException, InvalidOrderException, - OperationalException, PricingError, TemporaryError) +from freqtrade.exceptions import (DDosProtection, DependencyException, ExchangeError, + InvalidOrderException, OperationalException, PricingError, + TemporaryError) from freqtrade.exchange import (Binance, Bittrex, Exchange, Kraken, amount_to_precision, date_minus_candles, market_is_active, price_to_precision, timeframe_to_minutes, timeframe_to_msecs, timeframe_to_next_date, @@ -4179,17 +4180,24 @@ def test__fetch_and_calculate_funding_fees( type(api_mock).has = PropertyMock(return_value={'fetchOHLCV': True}) type(api_mock).has = PropertyMock(return_value={'fetchFundingRateHistory': True}) - exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange) + ex = get_patched_exchange(mocker, default_conf, api_mock, id=exchange) mocker.patch('freqtrade.exchange.Exchange.timeframes', PropertyMock( return_value=['1h', '4h', '8h'])) - funding_fees = exchange._fetch_and_calculate_funding_fees( + funding_fees = ex._fetch_and_calculate_funding_fees( pair='ADA/USDT', amount=amount, is_short=True, open_date=d1, close_date=d2) assert pytest.approx(funding_fees) == expected_fees # Fees for Longs are inverted - funding_fees = exchange._fetch_and_calculate_funding_fees( + funding_fees = ex._fetch_and_calculate_funding_fees( pair='ADA/USDT', amount=amount, is_short=False, open_date=d1, close_date=d2) assert pytest.approx(funding_fees) == -expected_fees + # Return empty "refresh_latest" + mocker.patch("freqtrade.exchange.Exchange.refresh_latest_ohlcv", return_value={}) + ex = get_patched_exchange(mocker, default_conf, api_mock, id=exchange) + with pytest.raises(ExchangeError, match="Could not find funding rates."): + ex._fetch_and_calculate_funding_fees( + pair='ADA/USDT', amount=amount, is_short=False, open_date=d1, close_date=d2) + @pytest.mark.parametrize('exchange,expected_fees', [ ('binance', -0.0009140999999999999), From 791f61c0899647f3c04ac072305e73187e266268 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 8 Sep 2022 07:13:15 +0000 Subject: [PATCH 71/96] Add test case for funding fee update failure --- freqtrade/freqtradebot.py | 2 -- tests/test_freqtradebot.py | 10 ++++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 61c323ed3..269c562d7 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -292,8 +292,6 @@ class FreqtradeBot(LoggingMixin): trade.funding_fees = funding_fees except ExchangeError: logger.warning("Could not update funding fees for open trades.") - else: - return 0.0 def startup_backpopulate_precision(self): diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 30b0b75b6..5f943504c 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -5430,6 +5430,16 @@ def test_update_funding_fees( )) +def test_update_funding_fees_error(mocker, default_conf, caplog): + mocker.patch('freqtrade.exchange.Exchange.get_funding_fees', side_effect=ExchangeError()) + default_conf['trading_mode'] = 'futures' + default_conf['margin_mode'] = 'isolated' + freqtrade = get_patched_freqtradebot(mocker, default_conf) + freqtrade.update_funding_fees() + + log_has("Could not update funding fees for open trades.", caplog) + + def test_position_adjust(mocker, default_conf_usdt, fee) -> None: patch_RPCManager(mocker) patch_exchange(mocker) From 9ef0ffe277e40159c207bb19782504d4bff492ff Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 8 Sep 2022 07:19:17 +0000 Subject: [PATCH 72/96] Update tests for funding-Fee exceptions --- freqtrade/freqtradebot.py | 3 ++- tests/test_freqtradebot.py | 8 ++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 269c562d7..6c001a8d6 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -740,11 +740,12 @@ class FreqtradeBot(LoggingMixin): # This is a new trade if trade is None: + funding_fees = 0.0 try: funding_fees = self.exchange.get_funding_fees( pair=pair, amount=amount, is_short=is_short, open_date=open_date) except ExchangeError: - logger.warning("Could not update funding fee.") + logger.warning("Could not find funding fee.") trade = Trade( pair=pair, diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 5f943504c..565797d81 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -506,7 +506,7 @@ def test_create_trades_multiple_trades( def test_create_trades_preopen(default_conf_usdt, ticker_usdt, fee, mocker, - limit_buy_order_usdt_open) -> None: + limit_buy_order_usdt_open, caplog) -> None: patch_RPCManager(mocker) patch_exchange(mocker) default_conf_usdt['max_open_trades'] = 4 @@ -515,6 +515,7 @@ def test_create_trades_preopen(default_conf_usdt, ticker_usdt, fee, mocker, fetch_ticker=ticker_usdt, create_order=MagicMock(return_value=limit_buy_order_usdt_open), get_fee=fee, + get_funding_fees=MagicMock(side_effect=ExchangeError()), ) freqtrade = FreqtradeBot(default_conf_usdt) patch_get_signal(freqtrade) @@ -522,6 +523,7 @@ def test_create_trades_preopen(default_conf_usdt, ticker_usdt, fee, mocker, # Create 2 existing trades freqtrade.execute_entry('ETH/USDT', default_conf_usdt['stake_amount']) freqtrade.execute_entry('NEO/BTC', default_conf_usdt['stake_amount']) + assert log_has("Could not find funding fee.", caplog) assert len(Trade.get_open_trades()) == 2 # Change order_id for new orders @@ -3666,7 +3668,7 @@ def test_may_execute_trade_exit_after_stoploss_on_exchange_hit( (True, 29.70297029, 2.2, 2.3, -8.63762376, -0.1443212, 'loss'), ]) def test_execute_trade_exit_market_order( - default_conf_usdt, ticker_usdt, fee, is_short, current_rate, amount, + default_conf_usdt, ticker_usdt, fee, is_short, current_rate, amount, caplog, limit, profit_amount, profit_ratio, profit_or_loss, ticker_usdt_sell_up, mocker ) -> None: """ @@ -3694,6 +3696,7 @@ def test_execute_trade_exit_market_order( fetch_ticker=ticker_usdt, get_fee=fee, _is_dry_limit_order_filled=MagicMock(return_value=True), + get_funding_fees=MagicMock(side_effect=ExchangeError()), ) patch_whitelist(mocker, default_conf_usdt) freqtrade = FreqtradeBot(default_conf_usdt) @@ -3719,6 +3722,7 @@ def test_execute_trade_exit_market_order( limit=ticker_usdt_sell_up()['ask' if is_short else 'bid'], exit_check=ExitCheckTuple(exit_type=ExitType.ROI) ) + assert log_has("Could not update funding fee.", caplog) assert not trade.is_open assert pytest.approx(trade.close_profit) == profit_ratio From 5e42defafc005de4d475776ae86ce6e242e8c8ed Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 8 Sep 2022 07:36:17 +0000 Subject: [PATCH 73/96] Update telegram docs to disable partial exit notifications --- docs/telegram-usage.md | 91 +++++++++++++++++++++--------------------- 1 file changed, 46 insertions(+), 45 deletions(-) diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index ece8700de..b9324def4 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -90,7 +90,8 @@ Example configuration showing the different settings: "trailing_stop_loss": "on", "stop_loss": "on", "stoploss_on_exchange": "on", - "custom_exit": "silent" + "custom_exit": "silent", + "partial_exit": "on" }, "entry_cancel": "silent", "exit_cancel": "on", @@ -138,7 +139,7 @@ You can create your own keyboard in `config.json`: "enabled": true, "token": "your_telegram_token", "chat_id": "your_telegram_chat_id", - "keyboard": [ + "keyboard": [ ["/daily", "/stats", "/balance", "/profit"], ["/status table", "/performance"], ["/reload_config", "/count", "/logs"] @@ -225,16 +226,16 @@ Once all positions are sold, run `/stop` to completely stop the bot. For each open trade, the bot will send you the following message. Enter Tag is configurable via Strategy. -> **Trade ID:** `123` `(since 1 days ago)` -> **Current Pair:** CVC/BTC +> **Trade ID:** `123` `(since 1 days ago)` +> **Current Pair:** CVC/BTC > **Direction:** Long > **Leverage:** 1.0 -> **Amount:** `26.64180098` +> **Amount:** `26.64180098` > **Enter Tag:** Awesome Long Signal -> **Open Rate:** `0.00007489` -> **Current Rate:** `0.00007489` -> **Current Profit:** `12.95%` -> **Stoploss:** `0.00007389 (-0.02%)` +> **Open Rate:** `0.00007489` +> **Current Rate:** `0.00007489` +> **Current Profit:** `12.95%` +> **Stoploss:** `0.00007389 (-0.02%)` ### /status table @@ -261,26 +262,26 @@ current max Return a summary of your profit/loss and performance. -> **ROI:** Close trades -> ∙ `0.00485701 BTC (2.2%) (15.2 Σ%)` -> ∙ `62.968 USD` -> **ROI:** All trades -> ∙ `0.00255280 BTC (1.5%) (6.43 Σ%)` -> ∙ `33.095 EUR` -> -> **Total Trade Count:** `138` -> **First Trade opened:** `3 days ago` -> **Latest Trade opened:** `2 minutes ago` -> **Avg. Duration:** `2:33:45` -> **Best Performing:** `PAY/BTC: 50.23%` -> **Trading volume:** `0.5 BTC` -> **Profit factor:** `1.04` -> **Max Drawdown:** `9.23% (0.01255 BTC)` +> **ROI:** Close trades +> ∙ `0.00485701 BTC (2.2%) (15.2 Σ%)` +> ∙ `62.968 USD` +> **ROI:** All trades +> ∙ `0.00255280 BTC (1.5%) (6.43 Σ%)` +> ∙ `33.095 EUR` +> +> **Total Trade Count:** `138` +> **First Trade opened:** `3 days ago` +> **Latest Trade opened:** `2 minutes ago` +> **Avg. Duration:** `2:33:45` +> **Best Performing:** `PAY/BTC: 50.23%` +> **Trading volume:** `0.5 BTC` +> **Profit factor:** `1.04` +> **Max Drawdown:** `9.23% (0.01255 BTC)` -The relative profit of `1.2%` is the average profit per trade. -The relative profit of `15.2 Σ%` is be based on the starting capital - so in this case, the starting capital was `0.00485701 * 1.152 = 0.00738 BTC`. -Starting capital is either taken from the `available_capital` setting, or calculated by using current wallet size - profits. -Profit Factor is calculated as gross profits / gross losses - and should serve as an overall metric for the strategy. +The relative profit of `1.2%` is the average profit per trade. +The relative profit of `15.2 Σ%` is be based on the starting capital - so in this case, the starting capital was `0.00485701 * 1.152 = 0.00738 BTC`. +Starting capital is either taken from the `available_capital` setting, or calculated by using current wallet size - profits. +Profit Factor is calculated as gross profits / gross losses - and should serve as an overall metric for the strategy. Max drawdown corresponds to the backtesting metric `Absolute Drawdown (Account)` - calculated as `(Absolute Drawdown) / (DrawdownHigh + startingBalance)`. ### /forceexit @@ -309,27 +310,27 @@ Note that for this to work, `force_entry_enable` needs to be set to true. ### /performance Return the performance of each crypto-currency the bot has sold. -> Performance: -> 1. `RCN/BTC 0.003 BTC (57.77%) (1)` -> 2. `PAY/BTC 0.0012 BTC (56.91%) (1)` -> 3. `VIB/BTC 0.0011 BTC (47.07%) (1)` -> 4. `SALT/BTC 0.0010 BTC (30.24%) (1)` -> 5. `STORJ/BTC 0.0009 BTC (27.24%) (1)` -> ... +> Performance: +> 1. `RCN/BTC 0.003 BTC (57.77%) (1)` +> 2. `PAY/BTC 0.0012 BTC (56.91%) (1)` +> 3. `VIB/BTC 0.0011 BTC (47.07%) (1)` +> 4. `SALT/BTC 0.0010 BTC (30.24%) (1)` +> 5. `STORJ/BTC 0.0009 BTC (27.24%) (1)` +> ... ### /balance Return the balance of all crypto-currency your have on the exchange. -> **Currency:** BTC -> **Available:** 3.05890234 -> **Balance:** 3.05890234 -> **Pending:** 0.0 +> **Currency:** BTC +> **Available:** 3.05890234 +> **Balance:** 3.05890234 +> **Pending:** 0.0 -> **Currency:** CVC -> **Available:** 86.64180098 -> **Balance:** 86.64180098 -> **Pending:** 0.0 +> **Currency:** CVC +> **Available:** 86.64180098 +> **Balance:** 86.64180098 +> **Pending:** 0.0 ### /daily @@ -376,7 +377,7 @@ Month (count) Profit BTC Profit USD Profit % Shows the current whitelist -> Using whitelist `StaticPairList` with 22 pairs +> Using whitelist `StaticPairList` with 22 pairs > `IOTA/BTC, NEO/BTC, TRX/BTC, VET/BTC, ADA/BTC, ETC/BTC, NCASH/BTC, DASH/BTC, XRP/BTC, XVG/BTC, EOS/BTC, LTC/BTC, OMG/BTC, BTG/BTC, LSK/BTC, ZEC/BTC, HOT/BTC, IOTX/BTC, XMR/BTC, AST/BTC, XLM/BTC, NANO/BTC` ### /blacklist [pair] @@ -386,7 +387,7 @@ If Pair is set, then this pair will be added to the pairlist. Also supports multiple pairs, separated by a space. Use `/reload_config` to reset the blacklist. -> Using blacklist `StaticPairList` with 2 pairs +> Using blacklist `StaticPairList` with 2 pairs >`DODGE/BTC`, `HOT/BTC`. ### /edge From 08726a264be3abdb69569170915c23cd1459c0b2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 8 Sep 2022 07:48:36 +0000 Subject: [PATCH 74/96] Update FAQ to point out multiple parallel positions per trade --- docs/faq.md | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/docs/faq.md b/docs/faq.md index 381bbceb5..a72268ef9 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -4,7 +4,7 @@ Freqtrade supports spot trading only. -### Can I open short positions? +### Can my bot open short positions? Freqtrade can open short positions in futures markets. This requires the strategy to be made for this - and `"trading_mode": "futures"` in the configuration. @@ -12,9 +12,9 @@ Please make sure to read the [relevant documentation page](leverage.md) first. In spot markets, you can in some cases use leveraged spot tokens, which reflect an inverted pair (eg. BTCUP/USD, BTCDOWN/USD, ETHBULL/USD, ETHBEAR/USD,...) which can be traded with Freqtrade. -### Can I trade options or futures? +### Can my bot trade options or futures? -Futures trading is supported for selected exchanges. +Futures trading is supported for selected exchanges. Please refer to the [documentation start page](index.md#supported-futures-exchanges-experimental) for an uptodate list of supported exchanges. ## Beginner Tips & Tricks @@ -22,6 +22,13 @@ Futures trading is supported for selected exchanges. ## Freqtrade common issues +### Can freqtrade open multiple positions on the same pair in parallel? + +No. Freqtrade will only open one position per pair at a time. +You can however use the [`adjust_trade_position()` callback](strategy-callbacks.md#adjust-trade-position) to adjust an open position. + +Backtesting provides an option for this in `--eps` - however this is only there to highlight "hidden" signals, and will not work in live. + ### The bot does not start Running the bot with `freqtrade trade --config config.json` shows the output `freqtrade: command not found`. @@ -30,7 +37,7 @@ This could be caused by the following reasons: * The virtual environment is not active. * Run `source .env/bin/activate` to activate the virtual environment. -* The installation did not work correctly. +* The installation did not complete successfully. * Please check the [Installation documentation](installation.md). ### I have waited 5 minutes, why hasn't the bot made any trades yet? @@ -67,7 +74,7 @@ This is not a bot-problem, but will also happen while manual trading. While freqtrade can handle this (it'll sell 99 COIN), fees are often below the minimum tradable lot-size (you can only trade full COIN, not 0.9 COIN). Leaving the dust (0.9 COIN) on the exchange makes usually sense, as the next time freqtrade buys COIN, it'll eat into the remaining small balance, this time selling everything it bought, and therefore slowly declining the dust balance (although it most likely will never reach exactly 0). -Where possible (e.g. on binance), the use of the exchange's dedicated fee currency will fix this. +Where possible (e.g. on binance), the use of the exchange's dedicated fee currency will fix this. On binance, it's sufficient to have BNB in your account, and have "Pay fees in BNB" enabled in your profile. Your BNB balance will slowly decline (as it's used to pay fees) - but you'll no longer encounter dust (Freqtrade will include the fees in the profit calculations). Other exchanges don't offer such possibilities, where it's simply something you'll have to accept or move to a different exchange. @@ -109,7 +116,7 @@ This warning can point to one of the below problems: ### I'm getting the "RESTRICTED_MARKET" message in the log -Currently known to happen for US Bittrex users. +Currently known to happen for US Bittrex users. Read [the Bittrex section about restricted markets](exchanges.md#restricted-markets) for more information. @@ -177,8 +184,8 @@ The GPU improvements would only apply to pandas-native calculations - or ones wr For hyperopt, freqtrade is using scikit-optimize, which is built on top of scikit-learn. Their statement about GPU support is [pretty clear](https://scikit-learn.org/stable/faq.html#will-you-add-gpu-support). -GPU's also are only good at crunching numbers (floating point operations). -For hyperopt, we need both number-crunching (find next parameters) and running python code (running backtesting). +GPU's also are only good at crunching numbers (floating point operations). +For hyperopt, we need both number-crunching (find next parameters) and running python code (running backtesting). As such, GPU's are not too well suited for most parts of hyperopt. The benefit of using GPU would therefore be pretty slim - and will not justify the complexity introduced by trying to add GPU support. @@ -219,9 +226,9 @@ already 8\*10^9\*10 evaluations. A roughly total of 80 billion evaluations. Did you run 100 000 evaluations? Congrats, you've done roughly 1 / 100 000 th of the search space, assuming that the bot never tests the same parameters more than once. -* The time it takes to run 1000 hyperopt epochs depends on things like: The available cpu, hard-disk, ram, timeframe, timerange, indicator settings, indicator count, amount of coins that hyperopt test strategies on and the resulting trade count - which can be 650 trades in a year or 100000 trades depending if the strategy aims for big profits by trading rarely or for many low profit trades. +* The time it takes to run 1000 hyperopt epochs depends on things like: The available cpu, hard-disk, ram, timeframe, timerange, indicator settings, indicator count, amount of coins that hyperopt test strategies on and the resulting trade count - which can be 650 trades in a year or 100000 trades depending if the strategy aims for big profits by trading rarely or for many low profit trades. -Example: 4% profit 650 times vs 0,3% profit a trade 10000 times in a year. If we assume you set the --timerange to 365 days. +Example: 4% profit 650 times vs 0,3% profit a trade 10000 times in a year. If we assume you set the --timerange to 365 days. Example: `freqtrade --config config.json --strategy SampleStrategy --hyperopt SampleHyperopt -e 1000 --timerange 20190601-20200601` From bc7295579ff89387a402a6255ff5acfa9c583231 Mon Sep 17 00:00:00 2001 From: robcaulk Date: Thu, 8 Sep 2022 22:22:50 +0200 Subject: [PATCH 75/96] improve docs, make example strat hyperoptable --- docs/freqai.md | 22 ++++++++++++-- freqtrade/templates/FreqaiExampleStrategy.py | 31 +++++++++++++------- 2 files changed, 39 insertions(+), 14 deletions(-) diff --git a/docs/freqai.md b/docs/freqai.md index 006fe1393..92e019dfe 100644 --- a/docs/freqai.md +++ b/docs/freqai.md @@ -538,12 +538,28 @@ for each pair, for each backtesting window within the expanded `--timerange`. ### Hyperopt -The [Hyperopt](hyperopt.md) module can be executed with some restrictions: +Users can hyperopt using the same command as typical [hyperopt](hyperopt.md): + +```bash +freqtrade hyperopt --hyperopt-loss SharpeHyperOptLoss --strategy FreqaiExampleStrategy --strategy-path freqtrade/templates --config config_examples/config_freqai.example.json --timerange 20220428-20220507 +``` + +Users need to have the data pre-downloaded in the same fashion as if they were doing a FreqAI [backtest](#backtesting). In addition, users must consider some restrictions when trying to [Hyperopt](hyperopt.md) FreqAI strategies: - The `--analyze-per-epoch` hyperopt parameter is not compatible with FreqAI. - It's not possible to hyperopt indicators in `populate_any_indicators()` function. This means that the user cannot optimize model parameters using hyperopt. Apart from this exception, it is possible to optimize all other [spaces](hyperopt.md#running-hyperopt-with-smaller-search-space). -- The [Backtesting](#backtesting) instructions also apply apply to Hyperopt. - +- The [Backtesting](#backtesting) instructions also apply to Hyperopt. + +The best method for combining hyperopt and FreqAI is to focus on hyperopting entry/exit thresholds/criteria. Users need to focus on hyperopting parameters that are not used in their FreqAI features. For example, users should not try to hyperopt rolling window lengths in their feature creation, or any of their FreqAI config which changes predictions. In order to efficiently hyperopt the FreqAI strategy, FreqAI stores predictions as dataframes and reuses them. Hence the requirement to hyperopt entry/exit thresholds/criteria only. + +A good example of a hyperoptable parameter in FreqAI is a value for `DI_values` beyond which we consider outliers and below which we consider inliers: + +```python +di_max = IntParameter(low=1, high=20, default=10, space='buy', optimize=True, load=True) +dataframe['outlier'] = np.where(dataframe['DI_values'] > self.di_max.value/10, 1, 0) +``` + +Which would help the user understand the appropriate Dissimilarity Index values for their particular parameter space. ### Deciding the size of the sliding training window and backtesting duration diff --git a/freqtrade/templates/FreqaiExampleStrategy.py b/freqtrade/templates/FreqaiExampleStrategy.py index 78132b06d..8b8e03af7 100644 --- a/freqtrade/templates/FreqaiExampleStrategy.py +++ b/freqtrade/templates/FreqaiExampleStrategy.py @@ -6,7 +6,7 @@ import talib.abstract as ta from pandas import DataFrame from technical import qtpylib -from freqtrade.strategy import IStrategy, merge_informative_pair +from freqtrade.strategy import IStrategy, merge_informative_pair, CategoricalParameter logger = logging.getLogger(__name__) @@ -29,9 +29,6 @@ class FreqaiExampleStrategy(IStrategy): "main_plot": {}, "subplots": { "prediction": {"prediction": {"color": "blue"}}, - "target_roi": { - "target_roi": {"color": "brown"}, - }, "do_predict": { "do_predict": {"color": "brown"}, }, @@ -45,6 +42,11 @@ class FreqaiExampleStrategy(IStrategy): startup_candle_count: int = 40 can_short = False + std_dev_multiplier_buy = CategoricalParameter( + [0.75, 1, 1.25, 1.5, 1.75], default=1.25, space="buy", optimize=True) + std_dev_multiplier_sell = CategoricalParameter( + [0.1, 0.25, 0.4], space="sell", default=0.2, optimize=True) + def informative_pairs(self): whitelist_pairs = self.dp.current_whitelist() corr_pairs = self.config["freqai"]["feature_parameters"]["include_corr_pairlist"] @@ -182,21 +184,26 @@ class FreqaiExampleStrategy(IStrategy): # `populate_any_indicators()` for each training period. dataframe = self.freqai.start(dataframe, metadata, self) - - dataframe["target_roi"] = dataframe["&-s_close_mean"] + dataframe["&-s_close_std"] * 1.25 - dataframe["sell_roi"] = dataframe["&-s_close_mean"] - dataframe["&-s_close_std"] * 1.25 + for val in self.std_dev_multiplier_buy.range: + dataframe[f'target_roi_{val}'] = dataframe["&-s_close_mean"] + \ + dataframe["&-s_close_std"] * val + for val in self.std_dev_multiplier_sell.range: + dataframe[f'sell_roi_{val}'] = dataframe["&-s_close_mean"] - \ + dataframe["&-s_close_std"] * val return dataframe def populate_entry_trend(self, df: DataFrame, metadata: dict) -> DataFrame: - enter_long_conditions = [df["do_predict"] == 1, df["&-s_close"] > df["target_roi"]] + enter_long_conditions = [df["do_predict"] == 1, df["&-s_close"] + > df[f"target_roi_{self.std_dev_multiplier_buy.value}"]] if enter_long_conditions: df.loc[ reduce(lambda x, y: x & y, enter_long_conditions), ["enter_long", "enter_tag"] ] = (1, "long") - enter_short_conditions = [df["do_predict"] == 1, df["&-s_close"] < df["sell_roi"]] + enter_short_conditions = [df["do_predict"] == 1, df["&-s_close"] + < df[f"sell_roi_{self.std_dev_multiplier_sell.value}"]] if enter_short_conditions: df.loc[ @@ -206,11 +213,13 @@ class FreqaiExampleStrategy(IStrategy): return df def populate_exit_trend(self, df: DataFrame, metadata: dict) -> DataFrame: - exit_long_conditions = [df["do_predict"] == 1, df["&-s_close"] < df["sell_roi"] * 0.25] + exit_long_conditions = [df["do_predict"] == 1, df["&-s_close"] < + df[f"sell_roi_{self.std_dev_multiplier_sell.value}"] * 0.25] if exit_long_conditions: df.loc[reduce(lambda x, y: x & y, exit_long_conditions), "exit_long"] = 1 - exit_short_conditions = [df["do_predict"] == 1, df["&-s_close"] > df["target_roi"] * 0.25] + exit_short_conditions = [df["do_predict"] == 1, df["&-s_close"] > + df[f"target_roi_{self.std_dev_multiplier_buy.value}"] * 0.25] if exit_short_conditions: df.loc[reduce(lambda x, y: x & y, exit_short_conditions), "exit_short"] = 1 From c5d918075881f62277f21854fdbd85fd94335024 Mon Sep 17 00:00:00 2001 From: robcaulk Date: Thu, 8 Sep 2022 22:35:52 +0200 Subject: [PATCH 76/96] isort --- freqtrade/templates/FreqaiExampleStrategy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/templates/FreqaiExampleStrategy.py b/freqtrade/templates/FreqaiExampleStrategy.py index 8b8e03af7..991792eb9 100644 --- a/freqtrade/templates/FreqaiExampleStrategy.py +++ b/freqtrade/templates/FreqaiExampleStrategy.py @@ -6,7 +6,7 @@ import talib.abstract as ta from pandas import DataFrame from technical import qtpylib -from freqtrade.strategy import IStrategy, merge_informative_pair, CategoricalParameter +from freqtrade.strategy import CategoricalParameter, IStrategy, merge_informative_pair logger = logging.getLogger(__name__) From 92d71ebdb730c16bcab4eb61e3516ea7a58e4825 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 9 Sep 2022 03:03:30 +0000 Subject: [PATCH 77/96] Bump python from 3.10.6-slim-bullseye to 3.10.7-slim-bullseye Bumps python from 3.10.6-slim-bullseye to 3.10.7-slim-bullseye. --- updated-dependencies: - dependency-name: python dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index e84a4d095..b3e5d5e88 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.10.6-slim-bullseye as base +FROM python:3.10.7-slim-bullseye as base # Setup env ENV LANG C.UTF-8 From d38cc061396d615832833fcbc880be5bdced8459 Mon Sep 17 00:00:00 2001 From: Wagner Costa Santos Date: Wed, 7 Sep 2022 18:52:58 -0300 Subject: [PATCH 78/96] Fix freqai backtesting time range issue --- freqtrade/freqai/data_kitchen.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/freqtrade/freqai/data_kitchen.py b/freqtrade/freqai/data_kitchen.py index 7670cfd45..01b2f1f3b 100644 --- a/freqtrade/freqai/data_kitchen.py +++ b/freqtrade/freqai/data_kitchen.py @@ -461,6 +461,20 @@ class FreqaiDataKitchen: return df + def remove_training_from_backtesting( + self + ) -> DataFrame: + """ + Function which takes the backtesting time range and + remove training data from dataframe + """ + tr = self.config["timerange"] + backtesting_timerange = TimeRange.parse_timerange(tr) + start = datetime.fromtimestamp(backtesting_timerange.startts, tz=timezone.utc) + df = self.return_dataframe + df = df.loc[df["date"] >= start, :] + return df + def principal_component_analysis(self) -> None: """ Performs Principal Component Analysis on the data for dimensionality reduction @@ -954,6 +968,7 @@ class FreqaiDataKitchen: to_keep = [col for col in dataframe.columns if not col.startswith("&")] self.return_dataframe = pd.concat([dataframe[to_keep], self.full_df], axis=1) + self.return_dataframe = self.remove_training_from_backtesting() self.full_df = DataFrame() return From 1b6410d7d1e20d4ede790ac46d4232d8cddaa181 Mon Sep 17 00:00:00 2001 From: Emre Date: Thu, 8 Sep 2022 14:12:19 +0300 Subject: [PATCH 79/96] Add XGBoostRegressor for freqAI, fix mypy errors --- .../prediction_models/BaseClassifierModel.py | 8 ++-- .../prediction_models/BaseRegressionModel.py | 8 ++-- .../prediction_models/BaseTensorFlowModel.py | 2 +- .../prediction_models/XGBoostRegressor.py | 46 +++++++++++++++++++ requirements-freqai.txt | 1 + tests/freqai/test_freqai_interface.py | 31 +++++++++++++ 6 files changed, 87 insertions(+), 9 deletions(-) create mode 100644 freqtrade/freqai/prediction_models/XGBoostRegressor.py diff --git a/freqtrade/freqai/prediction_models/BaseClassifierModel.py b/freqtrade/freqai/prediction_models/BaseClassifierModel.py index e51e26e0f..291bacc82 100644 --- a/freqtrade/freqai/prediction_models/BaseClassifierModel.py +++ b/freqtrade/freqai/prediction_models/BaseClassifierModel.py @@ -21,7 +21,7 @@ class BaseClassifierModel(IFreqaiModel): """ def train( - self, unfiltered_dataframe: DataFrame, pair: str, dk: FreqaiDataKitchen + self, unfiltered_dataframe: DataFrame, pair: str, dk: FreqaiDataKitchen, **kwargs ) -> Any: """ Filter the training data and train a model to it. Train makes heavy use of the datakitchen @@ -68,7 +68,7 @@ class BaseClassifierModel(IFreqaiModel): return model def predict( - self, unfiltered_dataframe: DataFrame, dk: FreqaiDataKitchen, first: bool = False + self, dataframe: DataFrame, dk: FreqaiDataKitchen, first: bool = False, **kwargs ) -> Tuple[DataFrame, npt.NDArray[np.int_]]: """ Filter the prediction features data and predict with it. @@ -79,9 +79,9 @@ class BaseClassifierModel(IFreqaiModel): data (NaNs) or felt uncertain about data (PCA and DI index) """ - dk.find_features(unfiltered_dataframe) + dk.find_features(dataframe) filtered_dataframe, _ = dk.filter_features( - unfiltered_dataframe, dk.training_features_list, training_filter=False + dataframe, dk.training_features_list, training_filter=False ) filtered_dataframe = dk.normalize_data_from_metadata(filtered_dataframe) dk.data_dictionary["prediction_features"] = filtered_dataframe diff --git a/freqtrade/freqai/prediction_models/BaseRegressionModel.py b/freqtrade/freqai/prediction_models/BaseRegressionModel.py index 45f0c2937..da6fba571 100644 --- a/freqtrade/freqai/prediction_models/BaseRegressionModel.py +++ b/freqtrade/freqai/prediction_models/BaseRegressionModel.py @@ -20,7 +20,7 @@ class BaseRegressionModel(IFreqaiModel): """ def train( - self, unfiltered_dataframe: DataFrame, pair: str, dk: FreqaiDataKitchen + self, unfiltered_dataframe: DataFrame, pair: str, dk: FreqaiDataKitchen, **kwargs ) -> Any: """ Filter the training data and train a model to it. Train makes heavy use of the datakitchen @@ -67,7 +67,7 @@ class BaseRegressionModel(IFreqaiModel): return model def predict( - self, unfiltered_dataframe: DataFrame, dk: FreqaiDataKitchen, first: bool = False + self, dataframe: DataFrame, dk: FreqaiDataKitchen, first: bool = False, **kwargs ) -> Tuple[DataFrame, npt.NDArray[np.int_]]: """ Filter the prediction features data and predict with it. @@ -78,9 +78,9 @@ class BaseRegressionModel(IFreqaiModel): data (NaNs) or felt uncertain about data (PCA and DI index) """ - dk.find_features(unfiltered_dataframe) + dk.find_features(dataframe) filtered_dataframe, _ = dk.filter_features( - unfiltered_dataframe, dk.training_features_list, training_filter=False + dataframe, dk.training_features_list, training_filter=False ) filtered_dataframe = dk.normalize_data_from_metadata(filtered_dataframe) dk.data_dictionary["prediction_features"] = filtered_dataframe diff --git a/freqtrade/freqai/prediction_models/BaseTensorFlowModel.py b/freqtrade/freqai/prediction_models/BaseTensorFlowModel.py index 66e6ec1fc..6fb49239b 100644 --- a/freqtrade/freqai/prediction_models/BaseTensorFlowModel.py +++ b/freqtrade/freqai/prediction_models/BaseTensorFlowModel.py @@ -17,7 +17,7 @@ class BaseTensorFlowModel(IFreqaiModel): """ def train( - self, unfiltered_dataframe: DataFrame, pair: str, dk: FreqaiDataKitchen + self, unfiltered_dataframe: DataFrame, pair: str, dk: FreqaiDataKitchen, **kwargs ) -> Any: """ Filter the training data and train a model to it. Train makes heavy use of the datakitchen diff --git a/freqtrade/freqai/prediction_models/XGBoostRegressor.py b/freqtrade/freqai/prediction_models/XGBoostRegressor.py new file mode 100644 index 000000000..a8f250d16 --- /dev/null +++ b/freqtrade/freqai/prediction_models/XGBoostRegressor.py @@ -0,0 +1,46 @@ +import logging +from typing import Any, Dict + +import xgboost as xgb + +from freqtrade.freqai.data_kitchen import FreqaiDataKitchen +from freqtrade.freqai.prediction_models.BaseRegressionModel import BaseRegressionModel + + +logger = logging.getLogger(__name__) + + +class XGBoostRegressor(BaseRegressionModel): + """ + User created prediction model. The class needs to override three necessary + functions, predict(), train(), fit(). The class inherits ModelHandler which + has its own DataHandler where data is held, saved, loaded, and managed. + """ + + def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: + """ + User sets up the training and test data to fit their desired model here + :param data_dictionary: the dictionary constructed by DataHandler to hold + all the training and test data/labels. + """ + + xgb.set_config(verbosity=2) + xgb.config_context(verbosity=2) + + X = data_dictionary["train_features"] + y = data_dictionary["train_labels"] + + if self.freqai_info.get("data_split_parameters", {}).get("test_size", 0.1) == 0: + eval_set = None + else: + eval_set = [(data_dictionary["test_features"], data_dictionary["test_labels"])] + + sample_weight = data_dictionary["train_weights"] + + xgb_model = self.get_init_model(dk.pair) + + model = xgb.XGBRegressor(**self.model_training_parameters) + + model.fit(X=X, y=y, sample_weight=sample_weight, eval_set=eval_set, xgb_model=xgb_model) + + return model diff --git a/requirements-freqai.txt b/requirements-freqai.txt index 26e4617af..e8d950382 100644 --- a/requirements-freqai.txt +++ b/requirements-freqai.txt @@ -6,3 +6,4 @@ scikit-learn==1.1.2 joblib==1.1.0 catboost==1.0.6; platform_machine != 'aarch64' lightgbm==3.3.2 +xgboost==1.6.2 diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index 5441b3c24..7783c00e7 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -172,6 +172,37 @@ def test_train_model_in_series_LightGBMClassifier(mocker, freqai_conf): shutil.rmtree(Path(freqai.dk.full_path)) +def test_train_model_in_series_XGBoostRegressor(mocker, freqai_conf): + freqai_conf.update({"timerange": "20180110-20180130"}) + freqai_conf.update({"freqaimodel": "XGBoostRegressor"}) + freqai_conf.update({"strategy": "freqai_test_strat"}) + + strategy = get_patched_freqai_strategy(mocker, freqai_conf) + exchange = get_patched_exchange(mocker, freqai_conf) + strategy.dp = DataProvider(freqai_conf, exchange) + strategy.freqai_info = freqai_conf.get("freqai", {}) + freqai = strategy.freqai + freqai.live = True + freqai.dk = FreqaiDataKitchen(freqai_conf) + timerange = TimeRange.parse_timerange("20180110-20180130") + freqai.dd.load_all_pair_histories(timerange, freqai.dk) + + freqai.dd.pair_dict = MagicMock() + + data_load_timerange = TimeRange.parse_timerange("20180110-20180130") + new_timerange = TimeRange.parse_timerange("20180120-20180130") + + freqai.train_model_in_series(new_timerange, "ADA/BTC", + strategy, freqai.dk, data_load_timerange) + + assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_model.joblib").is_file() + assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_metadata.json").is_file() + assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_trained_df.pkl").is_file() + assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_svm_model.joblib").is_file() + + shutil.rmtree(Path(freqai.dk.full_path)) + + def test_start_backtesting(mocker, freqai_conf): freqai_conf.update({"timerange": "20180120-20180130"}) freqai_conf.get("freqai", {}).update({"save_backtest_models": True}) From df6e43d2c599badef3247f8878cacec2a81f5f79 Mon Sep 17 00:00:00 2001 From: Emre Date: Fri, 9 Sep 2022 00:11:09 +0300 Subject: [PATCH 80/96] Add XGBoostRegressorMultiTarget class --- .../XGBoostRegressorMultiTarget.py | 43 +++++++++++++++++++ tests/freqai/test_freqai_interface.py | 31 +++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 freqtrade/freqai/prediction_models/XGBoostRegressorMultiTarget.py diff --git a/freqtrade/freqai/prediction_models/XGBoostRegressorMultiTarget.py b/freqtrade/freqai/prediction_models/XGBoostRegressorMultiTarget.py new file mode 100644 index 000000000..5283501d1 --- /dev/null +++ b/freqtrade/freqai/prediction_models/XGBoostRegressorMultiTarget.py @@ -0,0 +1,43 @@ +import logging +from typing import Any, Dict + +from sklearn.multioutput import MultiOutputRegressor +from xgboost import XGBRegressor + +from freqtrade.freqai.data_kitchen import FreqaiDataKitchen +from freqtrade.freqai.prediction_models.BaseRegressionModel import BaseRegressionModel + + +logger = logging.getLogger(__name__) + + +class XGBoostRegressorMultiTarget(BaseRegressionModel): + """ + User created prediction model. The class needs to override three necessary + functions, predict(), train(), fit(). The class inherits ModelHandler which + has its own DataHandler where data is held, saved, loaded, and managed. + """ + + def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: + """ + User sets up the training and test data to fit their desired model here + :param data_dictionary: the dictionary constructed by DataHandler to hold + all the training and test data/labels. + """ + + xgb = XGBRegressor(**self.model_training_parameters) + + X = data_dictionary["train_features"] + y = data_dictionary["train_labels"] + eval_set = (data_dictionary["test_features"], data_dictionary["test_labels"]) + sample_weight = data_dictionary["train_weights"] + + if self.continual_learning: + logger.warning('Continual learning not supported for MultiTarget models') + + model = MultiOutputRegressor(estimator=xgb) + model.fit(X=X, y=y, sample_weight=sample_weight) # , eval_set=eval_set) + train_score = model.score(X, y) + test_score = model.score(*eval_set) + logger.info(f"Train score {train_score}, Test score {test_score}") + return model diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index 7783c00e7..ff0eb24a9 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -203,6 +203,37 @@ def test_train_model_in_series_XGBoostRegressor(mocker, freqai_conf): shutil.rmtree(Path(freqai.dk.full_path)) +def test_train_model_in_series_XGBoostRegressorMultiModel(mocker, freqai_conf): + freqai_conf.update({"timerange": "20180110-20180130"}) + freqai_conf.update({"freqaimodel": "XGBoostRegressorMultiTarget"}) + freqai_conf.update({"strategy": "freqai_test_multimodel_strat"}) + strategy = get_patched_freqai_strategy(mocker, freqai_conf) + exchange = get_patched_exchange(mocker, freqai_conf) + strategy.dp = DataProvider(freqai_conf, exchange) + strategy.freqai_info = freqai_conf.get("freqai", {}) + freqai = strategy.freqai + freqai.live = True + freqai.dk = FreqaiDataKitchen(freqai_conf) + timerange = TimeRange.parse_timerange("20180110-20180130") + freqai.dd.load_all_pair_histories(timerange, freqai.dk) + + freqai.dd.pair_dict = MagicMock() + + data_load_timerange = TimeRange.parse_timerange("20180110-20180130") + new_timerange = TimeRange.parse_timerange("20180120-20180130") + + freqai.train_model_in_series(new_timerange, "ADA/BTC", strategy, freqai.dk, data_load_timerange) + + assert len(freqai.dk.label_list) == 2 + assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_model.joblib").is_file() + assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_metadata.json").is_file() + assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_trained_df.pkl").is_file() + assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_svm_model.joblib").is_file() + assert len(freqai.dk.data['training_features_list']) == 26 + + shutil.rmtree(Path(freqai.dk.full_path)) + + def test_start_backtesting(mocker, freqai_conf): freqai_conf.update({"timerange": "20180120-20180130"}) freqai_conf.get("freqai", {}).update({"save_backtest_models": True}) From acb410a0defb4626b52879b56e3d4ab8824c1085 Mon Sep 17 00:00:00 2001 From: Emre Date: Fri, 9 Sep 2022 00:11:43 +0300 Subject: [PATCH 81/96] Remove verbosity params --- freqtrade/freqai/prediction_models/XGBoostRegressor.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/freqtrade/freqai/prediction_models/XGBoostRegressor.py b/freqtrade/freqai/prediction_models/XGBoostRegressor.py index a8f250d16..acc4386f5 100644 --- a/freqtrade/freqai/prediction_models/XGBoostRegressor.py +++ b/freqtrade/freqai/prediction_models/XGBoostRegressor.py @@ -1,7 +1,7 @@ import logging from typing import Any, Dict -import xgboost as xgb +from xgboost import XGBRegressor from freqtrade.freqai.data_kitchen import FreqaiDataKitchen from freqtrade.freqai.prediction_models.BaseRegressionModel import BaseRegressionModel @@ -24,9 +24,6 @@ class XGBoostRegressor(BaseRegressionModel): all the training and test data/labels. """ - xgb.set_config(verbosity=2) - xgb.config_context(verbosity=2) - X = data_dictionary["train_features"] y = data_dictionary["train_labels"] @@ -39,7 +36,7 @@ class XGBoostRegressor(BaseRegressionModel): xgb_model = self.get_init_model(dk.pair) - model = xgb.XGBRegressor(**self.model_training_parameters) + model = XGBRegressor(**self.model_training_parameters) model.fit(X=X, y=y, sample_weight=sample_weight, eval_set=eval_set, xgb_model=xgb_model) From a826c0eb837af2f4f4b68660481e5c9f436caac7 Mon Sep 17 00:00:00 2001 From: robcaulk Date: Fri, 9 Sep 2022 19:17:15 +0200 Subject: [PATCH 82/96] ensure signatures match, reduce verbosity --- freqtrade/freqai/freqai_interface.py | 8 +++--- .../prediction_models/BaseClassifierModel.py | 26 +++++++++---------- .../prediction_models/BaseRegressionModel.py | 26 +++++++++---------- .../prediction_models/BaseTensorFlowModel.py | 10 +++---- 4 files changed, 35 insertions(+), 35 deletions(-) diff --git a/freqtrade/freqai/freqai_interface.py b/freqtrade/freqai/freqai_interface.py index 101df88ec..e4f77a9cf 100644 --- a/freqtrade/freqai/freqai_interface.py +++ b/freqtrade/freqai/freqai_interface.py @@ -673,12 +673,12 @@ class IFreqaiModel(ABC): # See freqai/prediction_models/CatboostPredictionModel.py for an example. @abstractmethod - def train(self, unfiltered_dataframe: DataFrame, pair: str, + def train(self, unfiltered_df: DataFrame, pair: str, dk: FreqaiDataKitchen, **kwargs) -> Any: """ Filter the training data and train a model to it. Train makes heavy use of the datahandler for storing, saving, loading, and analyzing the data. - :param unfiltered_dataframe: Full dataframe for the current training period + :param unfiltered_df: Full dataframe for the current training period :param metadata: pair metadata from strategy. :return: Trained model which can be used to inference (self.predict) """ @@ -697,11 +697,11 @@ class IFreqaiModel(ABC): @abstractmethod def predict( - self, dataframe: DataFrame, dk: FreqaiDataKitchen, first: bool = True, **kwargs + self, unfiltered_df: DataFrame, dk: FreqaiDataKitchen, **kwargs ) -> Tuple[DataFrame, NDArray[np.int_]]: """ Filter the prediction features data and predict with it. - :param unfiltered_dataframe: Full dataframe for the current backtest period. + :param unfiltered_df: Full dataframe for the current backtest period. :param dk: FreqaiDataKitchen = Data management/analysis tool associated to present pair only :param first: boolean = whether this is the first prediction or not. :return: diff --git a/freqtrade/freqai/prediction_models/BaseClassifierModel.py b/freqtrade/freqai/prediction_models/BaseClassifierModel.py index 291bacc82..5142ffb0d 100644 --- a/freqtrade/freqai/prediction_models/BaseClassifierModel.py +++ b/freqtrade/freqai/prediction_models/BaseClassifierModel.py @@ -21,12 +21,12 @@ class BaseClassifierModel(IFreqaiModel): """ def train( - self, unfiltered_dataframe: DataFrame, pair: str, dk: FreqaiDataKitchen, **kwargs + self, unfiltered_df: DataFrame, pair: str, dk: FreqaiDataKitchen, **kwargs ) -> Any: """ Filter the training data and train a model to it. Train makes heavy use of the datakitchen for storing, saving, loading, and analyzing the data. - :param unfiltered_dataframe: Full dataframe for the current training period + :param unfiltered_df: Full dataframe for the current training period :param metadata: pair metadata from strategy. :return: :model: Trained model which can be used to inference (self.predict) @@ -36,14 +36,14 @@ class BaseClassifierModel(IFreqaiModel): # filter the features requested by user in the configuration file and elegantly handle NaNs features_filtered, labels_filtered = dk.filter_features( - unfiltered_dataframe, + unfiltered_df, dk.training_features_list, dk.label_list, training_filter=True, ) - start_date = unfiltered_dataframe["date"].iloc[0].strftime("%Y-%m-%d") - end_date = unfiltered_dataframe["date"].iloc[-1].strftime("%Y-%m-%d") + start_date = unfiltered_df["date"].iloc[0].strftime("%Y-%m-%d") + end_date = unfiltered_df["date"].iloc[-1].strftime("%Y-%m-%d") logger.info(f"-------------------- Training on data from {start_date} to " f"{end_date}--------------------") # split data into train/test data. @@ -68,25 +68,25 @@ class BaseClassifierModel(IFreqaiModel): return model def predict( - self, dataframe: DataFrame, dk: FreqaiDataKitchen, first: bool = False, **kwargs + self, unfiltered_df: DataFrame, dk: FreqaiDataKitchen, **kwargs ) -> Tuple[DataFrame, npt.NDArray[np.int_]]: """ Filter the prediction features data and predict with it. - :param: unfiltered_dataframe: Full dataframe for the current backtest period. + :param: unfiltered_df: Full dataframe for the current backtest period. :return: :pred_df: dataframe containing the predictions :do_predict: np.array of 1s and 0s to indicate places where freqai needed to remove data (NaNs) or felt uncertain about data (PCA and DI index) """ - dk.find_features(dataframe) - filtered_dataframe, _ = dk.filter_features( - dataframe, dk.training_features_list, training_filter=False + dk.find_features(unfiltered_df) + filtered_df, _ = dk.filter_features( + unfiltered_df, dk.training_features_list, training_filter=False ) - filtered_dataframe = dk.normalize_data_from_metadata(filtered_dataframe) - dk.data_dictionary["prediction_features"] = filtered_dataframe + filtered_df = dk.normalize_data_from_metadata(filtered_df) + dk.data_dictionary["prediction_features"] = filtered_df - self.data_cleaning_predict(dk, filtered_dataframe) + self.data_cleaning_predict(dk, filtered_df) predictions = self.model.predict(dk.data_dictionary["prediction_features"]) pred_df = DataFrame(predictions, columns=dk.label_list) diff --git a/freqtrade/freqai/prediction_models/BaseRegressionModel.py b/freqtrade/freqai/prediction_models/BaseRegressionModel.py index da6fba571..1d87e42c0 100644 --- a/freqtrade/freqai/prediction_models/BaseRegressionModel.py +++ b/freqtrade/freqai/prediction_models/BaseRegressionModel.py @@ -20,12 +20,12 @@ class BaseRegressionModel(IFreqaiModel): """ def train( - self, unfiltered_dataframe: DataFrame, pair: str, dk: FreqaiDataKitchen, **kwargs + self, unfiltered_df: DataFrame, pair: str, dk: FreqaiDataKitchen, **kwargs ) -> Any: """ Filter the training data and train a model to it. Train makes heavy use of the datakitchen for storing, saving, loading, and analyzing the data. - :param unfiltered_dataframe: Full dataframe for the current training period + :param unfiltered_df: Full dataframe for the current training period :param metadata: pair metadata from strategy. :return: :model: Trained model which can be used to inference (self.predict) @@ -35,14 +35,14 @@ class BaseRegressionModel(IFreqaiModel): # filter the features requested by user in the configuration file and elegantly handle NaNs features_filtered, labels_filtered = dk.filter_features( - unfiltered_dataframe, + unfiltered_df, dk.training_features_list, dk.label_list, training_filter=True, ) - start_date = unfiltered_dataframe["date"].iloc[0].strftime("%Y-%m-%d") - end_date = unfiltered_dataframe["date"].iloc[-1].strftime("%Y-%m-%d") + start_date = unfiltered_df["date"].iloc[0].strftime("%Y-%m-%d") + end_date = unfiltered_df["date"].iloc[-1].strftime("%Y-%m-%d") logger.info(f"-------------------- Training on data from {start_date} to " f"{end_date}--------------------") # split data into train/test data. @@ -67,26 +67,26 @@ class BaseRegressionModel(IFreqaiModel): return model def predict( - self, dataframe: DataFrame, dk: FreqaiDataKitchen, first: bool = False, **kwargs + self, unfiltered_df: DataFrame, dk: FreqaiDataKitchen, **kwargs ) -> Tuple[DataFrame, npt.NDArray[np.int_]]: """ Filter the prediction features data and predict with it. - :param: unfiltered_dataframe: Full dataframe for the current backtest period. + :param: unfiltered_df: Full dataframe for the current backtest period. :return: :pred_df: dataframe containing the predictions :do_predict: np.array of 1s and 0s to indicate places where freqai needed to remove data (NaNs) or felt uncertain about data (PCA and DI index) """ - dk.find_features(dataframe) - filtered_dataframe, _ = dk.filter_features( - dataframe, dk.training_features_list, training_filter=False + dk.find_features(unfiltered_df) + filtered_df, _ = dk.filter_features( + unfiltered_df, dk.training_features_list, training_filter=False ) - filtered_dataframe = dk.normalize_data_from_metadata(filtered_dataframe) - dk.data_dictionary["prediction_features"] = filtered_dataframe + filtered_df = dk.normalize_data_from_metadata(filtered_df) + dk.data_dictionary["prediction_features"] = filtered_df # optional additional data cleaning/analysis - self.data_cleaning_predict(dk, filtered_dataframe) + self.data_cleaning_predict(dk, filtered_df) predictions = self.model.predict(dk.data_dictionary["prediction_features"]) pred_df = DataFrame(predictions, columns=dk.label_list) diff --git a/freqtrade/freqai/prediction_models/BaseTensorFlowModel.py b/freqtrade/freqai/prediction_models/BaseTensorFlowModel.py index 6fb49239b..eea80f3a2 100644 --- a/freqtrade/freqai/prediction_models/BaseTensorFlowModel.py +++ b/freqtrade/freqai/prediction_models/BaseTensorFlowModel.py @@ -17,12 +17,12 @@ class BaseTensorFlowModel(IFreqaiModel): """ def train( - self, unfiltered_dataframe: DataFrame, pair: str, dk: FreqaiDataKitchen, **kwargs + self, unfiltered_df: DataFrame, pair: str, dk: FreqaiDataKitchen, **kwargs ) -> Any: """ Filter the training data and train a model to it. Train makes heavy use of the datakitchen for storing, saving, loading, and analyzing the data. - :param unfiltered_dataframe: Full dataframe for the current training period + :param unfiltered_df: Full dataframe for the current training period :param metadata: pair metadata from strategy. :return: :model: Trained model which can be used to inference (self.predict) @@ -32,14 +32,14 @@ class BaseTensorFlowModel(IFreqaiModel): # filter the features requested by user in the configuration file and elegantly handle NaNs features_filtered, labels_filtered = dk.filter_features( - unfiltered_dataframe, + unfiltered_df, dk.training_features_list, dk.label_list, training_filter=True, ) - start_date = unfiltered_dataframe["date"].iloc[0].strftime("%Y-%m-%d") - end_date = unfiltered_dataframe["date"].iloc[-1].strftime("%Y-%m-%d") + start_date = unfiltered_df["date"].iloc[0].strftime("%Y-%m-%d") + end_date = unfiltered_df["date"].iloc[-1].strftime("%Y-%m-%d") logger.info(f"-------------------- Training on data from {start_date} to " f"{end_date}--------------------") # split data into train/test data. From c13bec26d1b01a6173396dcf1f4daed9236eaad8 Mon Sep 17 00:00:00 2001 From: robcaulk Date: Fri, 9 Sep 2022 19:41:28 +0200 Subject: [PATCH 83/96] add freqaimodel to hyperopt command --- docs/freqai.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/freqai.md b/docs/freqai.md index 92e019dfe..09a51c713 100644 --- a/docs/freqai.md +++ b/docs/freqai.md @@ -541,7 +541,7 @@ for each pair, for each backtesting window within the expanded `--timerange`. Users can hyperopt using the same command as typical [hyperopt](hyperopt.md): ```bash -freqtrade hyperopt --hyperopt-loss SharpeHyperOptLoss --strategy FreqaiExampleStrategy --strategy-path freqtrade/templates --config config_examples/config_freqai.example.json --timerange 20220428-20220507 +freqtrade hyperopt --hyperopt-loss SharpeHyperOptLoss --strategy FreqaiExampleStrategy --freqaimodel LightGBMRegressor --strategy-path freqtrade/templates --config config_examples/config_freqai.example.json --timerange 20220428-20220507 ``` Users need to have the data pre-downloaded in the same fashion as if they were doing a FreqAI [backtest](#backtesting). In addition, users must consider some restrictions when trying to [Hyperopt](hyperopt.md) FreqAI strategies: From 37fcbeba580143179c9344021ba2b5ceea516902 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 9 Sep 2022 19:58:55 +0200 Subject: [PATCH 84/96] Update backtesting output wording to Entries --- docs/backtesting.md | 74 ++++++++++++------------- freqtrade/optimize/optimize_reports.py | 5 +- tests/optimize/test_optimize_reports.py | 24 ++++---- 3 files changed, 52 insertions(+), 51 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index 8b2fdc345..f20a53d22 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -107,7 +107,7 @@ Strategy arguments: ## Test your strategy with Backtesting -Now you have good Buy and Sell strategies and some historic data, you want to test it against +Now you have good Entry and exit strategies and some historic data, you want to test it against real data. This is what we call [backtesting](https://en.wikipedia.org/wiki/Backtesting). Backtesting will use the crypto-currencies (pairs) from your config file and load historical candle (OHLCV) data from `user_data/data/` by default. @@ -215,7 +215,7 @@ Sometimes your account has certain fee rebates (fee reductions starting with a c To account for this in backtesting, you can use the `--fee` command line option to supply this value to backtesting. This fee must be a ratio, and will be applied twice (once for trade entry, and once for trade exit). -For example, if the buying and selling commission fee is 0.1% (i.e., 0.001 written as ratio), then you would run backtesting as the following: +For example, if the commission fee per order is 0.1% (i.e., 0.001 written as ratio), then you would run backtesting as the following: ```bash freqtrade backtesting --fee 0.001 @@ -252,41 +252,41 @@ The most important in the backtesting is to understand the result. A backtesting result will look like that: ``` -========================================================= BACKTESTING REPORT ========================================================== -| Pair | Buys | Avg Profit % | Cum Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Wins Draws Loss Win% | -|:---------|-------:|---------------:|---------------:|-----------------:|---------------:|:-------------|-------------------------:| -| ADA/BTC | 35 | -0.11 | -3.88 | -0.00019428 | -1.94 | 4:35:00 | 14 0 21 40.0 | -| ARK/BTC | 11 | -0.41 | -4.52 | -0.00022647 | -2.26 | 2:03:00 | 3 0 8 27.3 | -| BTS/BTC | 32 | 0.31 | 9.78 | 0.00048938 | 4.89 | 5:05:00 | 18 0 14 56.2 | -| DASH/BTC | 13 | -0.08 | -1.07 | -0.00005343 | -0.53 | 4:39:00 | 6 0 7 46.2 | -| ENG/BTC | 18 | 1.36 | 24.54 | 0.00122807 | 12.27 | 2:50:00 | 8 0 10 44.4 | -| EOS/BTC | 36 | 0.08 | 3.06 | 0.00015304 | 1.53 | 3:34:00 | 16 0 20 44.4 | -| ETC/BTC | 26 | 0.37 | 9.51 | 0.00047576 | 4.75 | 6:14:00 | 11 0 15 42.3 | -| ETH/BTC | 33 | 0.30 | 9.96 | 0.00049856 | 4.98 | 7:31:00 | 16 0 17 48.5 | -| IOTA/BTC | 32 | 0.03 | 1.09 | 0.00005444 | 0.54 | 3:12:00 | 14 0 18 43.8 | -| LSK/BTC | 15 | 1.75 | 26.26 | 0.00131413 | 13.13 | 2:58:00 | 6 0 9 40.0 | -| LTC/BTC | 32 | -0.04 | -1.38 | -0.00006886 | -0.69 | 4:49:00 | 11 0 21 34.4 | -| NANO/BTC | 17 | 1.26 | 21.39 | 0.00107058 | 10.70 | 1:55:00 | 10 0 7 58.5 | -| NEO/BTC | 23 | 0.82 | 18.97 | 0.00094936 | 9.48 | 2:59:00 | 10 0 13 43.5 | -| REQ/BTC | 9 | 1.17 | 10.54 | 0.00052734 | 5.27 | 3:47:00 | 4 0 5 44.4 | -| XLM/BTC | 16 | 1.22 | 19.54 | 0.00097800 | 9.77 | 3:15:00 | 7 0 9 43.8 | -| XMR/BTC | 23 | -0.18 | -4.13 | -0.00020696 | -2.07 | 5:30:00 | 12 0 11 52.2 | -| XRP/BTC | 35 | 0.66 | 22.96 | 0.00114897 | 11.48 | 3:49:00 | 12 0 23 34.3 | -| ZEC/BTC | 22 | -0.46 | -10.18 | -0.00050971 | -5.09 | 2:22:00 | 7 0 15 31.8 | -| TOTAL | 429 | 0.36 | 152.41 | 0.00762792 | 76.20 | 4:12:00 | 186 0 243 43.4 | +========================================================= BACKTESTING REPORT ========================================================= +| Pair | Entries | Avg Profit % | Cum Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Wins Draws Loss Win% | +|:---------|--------:|---------------:|---------------:|-----------------:|---------------:|:-------------|-------------------------:| +| ADA/BTC | 35 | -0.11 | -3.88 | -0.00019428 | -1.94 | 4:35:00 | 14 0 21 40.0 | +| ARK/BTC | 11 | -0.41 | -4.52 | -0.00022647 | -2.26 | 2:03:00 | 3 0 8 27.3 | +| BTS/BTC | 32 | 0.31 | 9.78 | 0.00048938 | 4.89 | 5:05:00 | 18 0 14 56.2 | +| DASH/BTC | 13 | -0.08 | -1.07 | -0.00005343 | -0.53 | 4:39:00 | 6 0 7 46.2 | +| ENG/BTC | 18 | 1.36 | 24.54 | 0.00122807 | 12.27 | 2:50:00 | 8 0 10 44.4 | +| EOS/BTC | 36 | 0.08 | 3.06 | 0.00015304 | 1.53 | 3:34:00 | 16 0 20 44.4 | +| ETC/BTC | 26 | 0.37 | 9.51 | 0.00047576 | 4.75 | 6:14:00 | 11 0 15 42.3 | +| ETH/BTC | 33 | 0.30 | 9.96 | 0.00049856 | 4.98 | 7:31:00 | 16 0 17 48.5 | +| IOTA/BTC | 32 | 0.03 | 1.09 | 0.00005444 | 0.54 | 3:12:00 | 14 0 18 43.8 | +| LSK/BTC | 15 | 1.75 | 26.26 | 0.00131413 | 13.13 | 2:58:00 | 6 0 9 40.0 | +| LTC/BTC | 32 | -0.04 | -1.38 | -0.00006886 | -0.69 | 4:49:00 | 11 0 21 34.4 | +| NANO/BTC | 17 | 1.26 | 21.39 | 0.00107058 | 10.70 | 1:55:00 | 10 0 7 58.5 | +| NEO/BTC | 23 | 0.82 | 18.97 | 0.00094936 | 9.48 | 2:59:00 | 10 0 13 43.5 | +| REQ/BTC | 9 | 1.17 | 10.54 | 0.00052734 | 5.27 | 3:47:00 | 4 0 5 44.4 | +| XLM/BTC | 16 | 1.22 | 19.54 | 0.00097800 | 9.77 | 3:15:00 | 7 0 9 43.8 | +| XMR/BTC | 23 | -0.18 | -4.13 | -0.00020696 | -2.07 | 5:30:00 | 12 0 11 52.2 | +| XRP/BTC | 35 | 0.66 | 22.96 | 0.00114897 | 11.48 | 3:49:00 | 12 0 23 34.3 | +| ZEC/BTC | 22 | -0.46 | -10.18 | -0.00050971 | -5.09 | 2:22:00 | 7 0 15 31.8 | +| TOTAL | 429 | 0.36 | 152.41 | 0.00762792 | 76.20 | 4:12:00 | 186 0 243 43.4 | ========================================================= EXIT REASON STATS ========================================================== -| Exit Reason | Sells | Wins | Draws | Losses | +| Exit Reason | Exits | Wins | Draws | Losses | |:-------------------|--------:|------:|-------:|--------:| | trailing_stop_loss | 205 | 150 | 0 | 55 | | stop_loss | 166 | 0 | 0 | 166 | | exit_signal | 56 | 36 | 0 | 20 | | force_exit | 2 | 0 | 0 | 2 | ====================================================== LEFT OPEN TRADES REPORT ====================================================== -| Pair | Buys | Avg Profit % | Cum Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Win Draw Loss Win% | -|:---------|-------:|---------------:|---------------:|-----------------:|---------------:|:---------------|--------------------:| -| ADA/BTC | 1 | 0.89 | 0.89 | 0.00004434 | 0.44 | 6:00:00 | 1 0 0 100 | -| LTC/BTC | 1 | 0.68 | 0.68 | 0.00003421 | 0.34 | 2:00:00 | 1 0 0 100 | -| TOTAL | 2 | 0.78 | 1.57 | 0.00007855 | 0.78 | 4:00:00 | 2 0 0 100 | +| Pair | Entries | Avg Profit % | Cum Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Win Draw Loss Win% | +|:---------|---------:|---------------:|---------------:|-----------------:|---------------:|:---------------|--------------------:| +| ADA/BTC | 1 | 0.89 | 0.89 | 0.00004434 | 0.44 | 6:00:00 | 1 0 0 100 | +| LTC/BTC | 1 | 0.68 | 0.68 | 0.00003421 | 0.34 | 2:00:00 | 1 0 0 100 | +| TOTAL | 2 | 0.78 | 1.57 | 0.00007855 | 0.78 | 4:00:00 | 2 0 0 100 | ================== SUMMARY METRICS ================== | Metric | Value | |-----------------------------+---------------------| @@ -356,7 +356,7 @@ The column `Avg Profit %` shows the average profit for all trades made while the The column `Tot Profit %` shows instead the total profit % in relation to the starting balance. In the above results, we have a starting balance of 0.01 BTC and an absolute profit of 0.00762792 BTC - so the `Tot Profit %` will be `(0.00762792 / 0.01) * 100 ~= 76.2%`. -Your strategy performance is influenced by your buy strategy, your exit strategy, and also by the `minimal_roi` and `stop_loss` you have set. +Your strategy performance is influenced by your entry strategy, your exit strategy, and also by the `minimal_roi` and `stop_loss` you have set. For example, if your `minimal_roi` is only `"0": 0.01` you cannot expect the bot to make more profit than 1% (because it will exit every time a trade reaches 1%). @@ -515,7 +515,7 @@ You can then load the trades to perform further analysis as shown in the [data a Since backtesting lacks some detailed information about what happens within a candle, it needs to take a few assumptions: - Exchange [trading limits](#trading-limits-in-backtesting) are respected -- Buys happen at open-price +- Entries happen at open-price - All orders are filled at the requested price (no slippage, no unfilled orders) - Exit-signal exits happen at open-price of the consecutive candle - Exit-signal is favored over Stoploss, because exit-signals are assumed to trigger on candle's open @@ -612,11 +612,11 @@ There will be an additional table comparing win/losses of the different strategi Detailed output for all strategies one after the other will be available, so make sure to scroll up to see the details per strategy. ``` -=========================================================== STRATEGY SUMMARY ========================================================================= -| Strategy | Buys | Avg Profit % | Cum Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Wins | Draws | Losses | Drawdown % | -|:------------|-------:|---------------:|---------------:|-----------------:|---------------:|:---------------|------:|-------:|-------:|-----------:| -| Strategy1 | 429 | 0.36 | 152.41 | 0.00762792 | 76.20 | 4:12:00 | 186 | 0 | 243 | 45.2 | -| Strategy2 | 1487 | -0.13 | -197.58 | -0.00988917 | -98.79 | 4:43:00 | 662 | 0 | 825 | 241.68 | +=========================================================== STRATEGY SUMMARY =========================================================================== +| Strategy | Entries | Avg Profit % | Cum Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Wins | Draws | Losses | Drawdown % | +|:------------|---------:|---------------:|---------------:|-----------------:|---------------:|:---------------|------:|-------:|-------:|-----------:| +| Strategy1 | 429 | 0.36 | 152.41 | 0.00762792 | 76.20 | 4:12:00 | 186 | 0 | 243 | 45.2 | +| Strategy2 | 1487 | -0.13 | -197.58 | -0.00988917 | -98.79 | 4:43:00 | 662 | 0 | 825 | 241.68 | ``` ## Next step diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 519022db2..fa6c3f161 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -75,7 +75,8 @@ def _get_line_floatfmt(stake_currency: str) -> List[str]: '.2f', 'd', 's', 's'] -def _get_line_header(first_column: str, stake_currency: str, direction: str = 'Buys') -> List[str]: +def _get_line_header(first_column: str, stake_currency: str, + direction: str = 'Entries') -> List[str]: """ Generate header lines (goes in line with _generate_result_line()) """ @@ -642,7 +643,7 @@ def text_table_tags(tag_type: str, tag_results: List[Dict[str, Any]], stake_curr if (tag_type == "enter_tag"): headers = _get_line_header("TAG", stake_currency) else: - headers = _get_line_header("TAG", stake_currency, 'Sells') + headers = _get_line_header("TAG", stake_currency, 'Exits') floatfmt = _get_line_floatfmt(stake_currency) output = [ [ diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index 562e12820..5095f2fde 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -40,14 +40,14 @@ def test_text_table_bt_results(): ) result_str = ( - '| Pair | Buys | Avg Profit % | Cum Profit % | Tot Profit BTC | Tot Profit % |' - ' Avg Duration | Win Draw Loss Win% |\n' - '|---------+--------+----------------+----------------+------------------+----------------+' - '----------------+-------------------------|\n' - '| ETH/BTC | 3 | 8.33 | 25.00 | 0.50000000 | 12.50 |' - ' 0:20:00 | 2 0 1 66.7 |\n' - '| TOTAL | 3 | 8.33 | 25.00 | 0.50000000 | 12.50 |' - ' 0:20:00 | 2 0 1 66.7 |' + '| Pair | Entries | Avg Profit % | Cum Profit % | Tot Profit BTC | ' + 'Tot Profit % | Avg Duration | Win Draw Loss Win% |\n' + '|---------+-----------+----------------+----------------+------------------+' + '----------------+----------------+-------------------------|\n' + '| ETH/BTC | 3 | 8.33 | 25.00 | 0.50000000 | ' + '12.50 | 0:20:00 | 2 0 1 66.7 |\n' + '| TOTAL | 3 | 8.33 | 25.00 | 0.50000000 | ' + '12.50 | 0:20:00 | 2 0 1 66.7 |' ) pair_results = generate_pair_metrics(['ETH/BTC'], stake_currency='BTC', @@ -402,13 +402,13 @@ def test_text_table_strategy(testdatadir): bt_res_data_comparison = bt_res_data.pop('strategy_comparison') result_str = ( - '| Strategy | Buys | Avg Profit % | Cum Profit % | Tot Profit BTC |' + '| Strategy | Entries | Avg Profit % | Cum Profit % | Tot Profit BTC |' ' Tot Profit % | Avg Duration | Win Draw Loss Win% | Drawdown |\n' - '|----------------+--------+----------------+----------------+------------------+' + '|----------------+-----------+----------------+----------------+------------------+' '----------------+----------------+-------------------------+-----------------------|\n' - '| StrategyTestV2 | 179 | 0.08 | 14.39 | 0.02608550 |' + '| StrategyTestV2 | 179 | 0.08 | 14.39 | 0.02608550 |' ' 260.85 | 3:40:00 | 170 0 9 95.0 | 0.00308222 BTC 8.67% |\n' - '| TestStrategy | 179 | 0.08 | 14.39 | 0.02608550 |' + '| TestStrategy | 179 | 0.08 | 14.39 | 0.02608550 |' ' 260.85 | 3:40:00 | 170 0 9 95.0 | 0.00308222 BTC 8.67% |' ) From 97be3318f447445dfaab52f8b4d08ba2f8e59112 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 9 Sep 2022 20:17:34 +0200 Subject: [PATCH 85/96] Use Datetime_format from constants --- freqtrade/data/history/history_utils.py | 8 ++++---- freqtrade/freqai/freqai_interface.py | 5 +++-- tests/data/test_history.py | 6 +++--- tests/test_persistence.py | 10 +++++----- 4 files changed, 15 insertions(+), 14 deletions(-) diff --git a/freqtrade/data/history/history_utils.py b/freqtrade/data/history/history_utils.py index 7a3fa4e0c..6a6e29429 100644 --- a/freqtrade/data/history/history_utils.py +++ b/freqtrade/data/history/history_utils.py @@ -228,9 +228,9 @@ def _download_pair_history(pair: str, *, ) logger.debug("Current Start: %s", - f"{data.iloc[0]['date']:%Y-%m-%d %H:%M:%S}" if not data.empty else 'None') + f"{data.iloc[0]['date']:DATETIME_PRINT_FORMAT}" if not data.empty else 'None') logger.debug("Current End: %s", - f"{data.iloc[-1]['date']:%Y-%m-%d %H:%M:%S}" if not data.empty else 'None') + f"{data.iloc[-1]['date']:DATETIME_PRINT_FORMAT}" if not data.empty else 'None') # Default since_ms to 30 days if nothing is given new_data = exchange.get_historic_ohlcv(pair=pair, @@ -254,9 +254,9 @@ def _download_pair_history(pair: str, *, fill_missing=False, drop_incomplete=False) logger.debug("New Start: %s", - f"{data.iloc[0]['date']:%Y-%m-%d %H:%M:%S}" if not data.empty else 'None') + f"{data.iloc[0]['date']:DATETIME_PRINT_FORMAT}" if not data.empty else 'None') logger.debug("New End: %s", - f"{data.iloc[-1]['date']:%Y-%m-%d %H:%M:%S}" if not data.empty else 'None') + f"{data.iloc[-1]['date']:DATETIME_PRINT_FORMAT}" if not data.empty else 'None') data_handler.ohlcv_store(pair, timeframe, data=data, candle_type=candle_type) return True diff --git a/freqtrade/freqai/freqai_interface.py b/freqtrade/freqai/freqai_interface.py index c5ac17a3a..7a48f6e15 100644 --- a/freqtrade/freqai/freqai_interface.py +++ b/freqtrade/freqai/freqai_interface.py @@ -14,6 +14,7 @@ from numpy.typing import NDArray from pandas import DataFrame from freqtrade.configuration import TimeRange +from freqtrade.constants import DATETIME_PRINT_FORMAT from freqtrade.enums import RunMode from freqtrade.exceptions import OperationalException from freqtrade.exchange import timeframe_to_seconds @@ -232,10 +233,10 @@ class IFreqaiModel(ABC): trained_timestamp = tr_train tr_train_startts_str = datetime.fromtimestamp( tr_train.startts, - tz=timezone.utc).strftime("%Y-%m-%d %H:%M:%S") + tz=timezone.utc).strftime(DATETIME_PRINT_FORMAT) tr_train_stopts_str = datetime.fromtimestamp( tr_train.stopts, - tz=timezone.utc).strftime("%Y-%m-%d %H:%M:%S") + tz=timezone.utc).strftime(DATETIME_PRINT_FORMAT) logger.info( f"Training {metadata['pair']}, {self.pair_it}/{self.total_pairs} pairs" f" from {tr_train_startts_str} to {tr_train_stopts_str}, {train_it}/{total_trains} " diff --git a/tests/data/test_history.py b/tests/data/test_history.py index 9709e7ad0..8081e984f 100644 --- a/tests/data/test_history.py +++ b/tests/data/test_history.py @@ -13,7 +13,7 @@ from pandas import DataFrame from pandas.testing import assert_frame_equal from freqtrade.configuration import TimeRange -from freqtrade.constants import AVAILABLE_DATAHANDLERS +from freqtrade.constants import AVAILABLE_DATAHANDLERS, DATETIME_PRINT_FORMAT from freqtrade.data.converter import ohlcv_to_dataframe from freqtrade.data.history.hdf5datahandler import HDF5DataHandler from freqtrade.data.history.history_utils import (_download_pair_history, _download_trades_history, @@ -386,7 +386,7 @@ def test_load_partial_missing(testdatadir, caplog) -> None: assert td != len(data['UNITTEST/BTC']) start_real = data['UNITTEST/BTC'].iloc[0, 0] assert log_has(f'UNITTEST/BTC, spot, 5m, ' - f'data starts at {start_real.strftime("%Y-%m-%d %H:%M:%S")}', + f'data starts at {start_real.strftime(DATETIME_PRINT_FORMAT)}', caplog) # Make sure we start fresh - test missing data at end caplog.clear() @@ -401,7 +401,7 @@ def test_load_partial_missing(testdatadir, caplog) -> None: # Shift endtime with +5 - as last candle is dropped (partial candle) end_real = arrow.get(data['UNITTEST/BTC'].iloc[-1, 0]).shift(minutes=5) assert log_has(f'UNITTEST/BTC, spot, 5m, ' - f'data ends at {end_real.strftime("%Y-%m-%d %H:%M:%S")}', + f'data ends at {end_real.strftime(DATETIME_PRINT_FORMAT)}', caplog) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index cdca3bc4d..e7f218c02 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -9,7 +9,7 @@ import arrow import pytest from sqlalchemy import create_engine, text -from freqtrade import constants +from freqtrade.constants import DATETIME_PRINT_FORMAT, DEFAULT_DB_PROD_URL from freqtrade.enums import TradingMode from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.persistence import LocalTrade, Order, Trade, init_db @@ -52,7 +52,7 @@ def test_init_invalid_db_url(): def test_init_prod_db(default_conf, mocker): default_conf.update({'dry_run': False}) - default_conf.update({'db_url': constants.DEFAULT_DB_PROD_URL}) + default_conf.update({'db_url': DEFAULT_DB_PROD_URL}) create_engine_mock = mocker.patch('freqtrade.persistence.models.create_engine', MagicMock()) @@ -1739,7 +1739,7 @@ def test_to_json(fee): 'base_currency': 'ADA', 'quote_currency': 'USDT', 'is_open': None, - 'open_date': trade.open_date.strftime("%Y-%m-%d %H:%M:%S"), + 'open_date': trade.open_date.strftime(DATETIME_PRINT_FORMAT), 'open_timestamp': int(trade.open_date.timestamp() * 1000), 'open_order_id': 'dry_run_buy_12345', 'close_date': None, @@ -1817,9 +1817,9 @@ def test_to_json(fee): 'pair': 'XRP/BTC', 'base_currency': 'XRP', 'quote_currency': 'BTC', - 'open_date': trade.open_date.strftime("%Y-%m-%d %H:%M:%S"), + 'open_date': trade.open_date.strftime(DATETIME_PRINT_FORMAT), 'open_timestamp': int(trade.open_date.timestamp() * 1000), - 'close_date': trade.close_date.strftime("%Y-%m-%d %H:%M:%S"), + 'close_date': trade.close_date.strftime(DATETIME_PRINT_FORMAT), 'close_timestamp': int(trade.close_date.timestamp() * 1000), 'open_rate': 0.123, 'close_rate': 0.125, From 939fb7acb338202dfa2bf945ee51d95e37e14e97 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 9 Sep 2022 20:31:30 +0200 Subject: [PATCH 86/96] Fix enum imports --- freqtrade/commands/db_commands.py | 2 +- freqtrade/exchange/okx.py | 3 +-- freqtrade/rpc/discord.py | 2 +- freqtrade/strategy/interface.py | 5 ++--- freqtrade/strategy/parameters.py | 2 +- tests/exchange/test_okx.py | 3 +-- tests/strategy/test_interface.py | 3 +-- 7 files changed, 8 insertions(+), 12 deletions(-) diff --git a/freqtrade/commands/db_commands.py b/freqtrade/commands/db_commands.py index 618b5cb6e..c424016b1 100644 --- a/freqtrade/commands/db_commands.py +++ b/freqtrade/commands/db_commands.py @@ -4,7 +4,7 @@ from typing import Any, Dict from sqlalchemy import func from freqtrade.configuration.config_setup import setup_utils_configuration -from freqtrade.enums.runmode import RunMode +from freqtrade.enums import RunMode logger = logging.getLogger(__name__) diff --git a/freqtrade/exchange/okx.py b/freqtrade/exchange/okx.py index 9340dd0e4..49f8ea107 100644 --- a/freqtrade/exchange/okx.py +++ b/freqtrade/exchange/okx.py @@ -4,8 +4,7 @@ from typing import Dict, List, Optional, Tuple import ccxt from freqtrade.constants import BuySell -from freqtrade.enums import MarginMode, TradingMode -from freqtrade.enums.candletype import CandleType +from freqtrade.enums import CandleType, MarginMode, TradingMode from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError from freqtrade.exchange import Exchange, date_minus_candles from freqtrade.exchange.common import retrier diff --git a/freqtrade/rpc/discord.py b/freqtrade/rpc/discord.py index 5991f7126..85acfae4e 100644 --- a/freqtrade/rpc/discord.py +++ b/freqtrade/rpc/discord.py @@ -1,7 +1,7 @@ import logging from typing import Any, Dict -from freqtrade.enums.rpcmessagetype import RPCMessageType +from freqtrade.enums import RPCMessageType from freqtrade.rpc import RPC from freqtrade.rpc.webhook import Webhook diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 70cc7fdb3..9401ebebe 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -12,9 +12,8 @@ from pandas import DataFrame from freqtrade.constants import ListPairsWithTimeframes from freqtrade.data.dataprovider import DataProvider -from freqtrade.enums import (CandleType, ExitCheckTuple, ExitType, SignalDirection, SignalTagType, - SignalType, TradingMode) -from freqtrade.enums.runmode import RunMode +from freqtrade.enums import (CandleType, ExitCheckTuple, ExitType, RunMode, SignalDirection, + SignalTagType, SignalType, TradingMode) from freqtrade.exceptions import OperationalException, StrategyError from freqtrade.exchange import timeframe_to_minutes, timeframe_to_next_date, timeframe_to_seconds from freqtrade.persistence import Order, PairLocks, Trade diff --git a/freqtrade/strategy/parameters.py b/freqtrade/strategy/parameters.py index c6037ae0b..796fb9514 100644 --- a/freqtrade/strategy/parameters.py +++ b/freqtrade/strategy/parameters.py @@ -7,7 +7,7 @@ from abc import ABC, abstractmethod from contextlib import suppress from typing import Any, Optional, Sequence, Union -from freqtrade.enums.hyperoptstate import HyperoptState +from freqtrade.enums import HyperoptState from freqtrade.optimize.hyperopt_tools import HyperoptStateContainer diff --git a/tests/exchange/test_okx.py b/tests/exchange/test_okx.py index b475b84ff..12322acae 100644 --- a/tests/exchange/test_okx.py +++ b/tests/exchange/test_okx.py @@ -4,8 +4,7 @@ from unittest.mock import MagicMock, PropertyMock import pytest -from freqtrade.enums import MarginMode, TradingMode -from freqtrade.enums.candletype import CandleType +from freqtrade.enums import CandleType, MarginMode, TradingMode from freqtrade.exchange.exchange import timeframe_to_minutes from tests.conftest import get_mock_coro, get_patched_exchange, log_has from tests.exchange.test_exchange import ccxt_exceptionhandlers diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 65ee05d71..070e78b1d 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -11,8 +11,7 @@ from pandas import DataFrame from freqtrade.configuration import TimeRange from freqtrade.data.dataprovider import DataProvider from freqtrade.data.history import load_data -from freqtrade.enums import ExitCheckTuple, ExitType, SignalDirection -from freqtrade.enums.hyperoptstate import HyperoptState +from freqtrade.enums import ExitCheckTuple, ExitType, HyperoptState, SignalDirection from freqtrade.exceptions import OperationalException, StrategyError from freqtrade.optimize.hyperopt_tools import HyperoptStateContainer from freqtrade.optimize.space import SKDecimal From 05581db4e387ae8c1b2ae7d097e3b20f349e4b16 Mon Sep 17 00:00:00 2001 From: robcaulk Date: Fri, 9 Sep 2022 20:37:21 +0200 Subject: [PATCH 87/96] ensure columns are only dropped in training --- freqtrade/freqai/data_kitchen.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/freqtrade/freqai/data_kitchen.py b/freqtrade/freqai/data_kitchen.py index 790b3c078..1af01cb6b 100644 --- a/freqtrade/freqai/data_kitchen.py +++ b/freqtrade/freqai/data_kitchen.py @@ -209,14 +209,13 @@ class FreqaiDataKitchen: filtered_df = unfiltered_df.filter(training_feature_list, axis=1) filtered_df = filtered_df.replace([np.inf, -np.inf], np.nan) - const_cols = list((filtered_df.nunique() == 1).loc[lambda x: x].index) - if const_cols: - filtered_df = filtered_df.filter(filtered_df.columns.difference(const_cols)) - logger.warning(f"Removed features {const_cols} with constant values.") - drop_index = pd.isnull(filtered_df).any(1) # get the rows that have NaNs, drop_index = drop_index.replace(True, 1).replace(False, 0) # pep8 requirement. if (training_filter): + const_cols = list((filtered_df.nunique() == 1).loc[lambda x: x].index) + if const_cols: + filtered_df = filtered_df.filter(filtered_df.columns.difference(const_cols)) + logger.warning(f"Removed features {const_cols} with constant values.") # we don't care about total row number (total no. datapoints) in training, we only care # about removing any row with NaNs # if labels has multiple columns (user wants to train multiple modelEs), we detect here From 170bec0438e10955052fe7782ede7a42a2310cc8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 10 Sep 2022 08:24:23 +0200 Subject: [PATCH 88/96] Fix failing XGBoost tests --- tests/freqai/test_freqai_interface.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index d95ead90c..5f8eeb086 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -174,7 +174,7 @@ def test_extract_data_and_train_model_LightGBMClassifier(mocker, freqai_conf): shutil.rmtree(Path(freqai.dk.full_path)) -def test_train_model_in_series_XGBoostRegressor(mocker, freqai_conf): +def test_extract_data_and_train_model_XGBoostRegressor(mocker, freqai_conf): freqai_conf.update({"timerange": "20180110-20180130"}) freqai_conf.update({"freqaimodel": "XGBoostRegressor"}) freqai_conf.update({"strategy": "freqai_test_strat"}) @@ -194,8 +194,8 @@ def test_train_model_in_series_XGBoostRegressor(mocker, freqai_conf): data_load_timerange = TimeRange.parse_timerange("20180110-20180130") new_timerange = TimeRange.parse_timerange("20180120-20180130") - freqai.train_model_in_series(new_timerange, "ADA/BTC", - strategy, freqai.dk, data_load_timerange) + freqai.extract_data_and_train_model( + new_timerange, "ADA/BTC", strategy, freqai.dk, data_load_timerange) assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_model.joblib").is_file() assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_metadata.json").is_file() @@ -205,7 +205,7 @@ def test_train_model_in_series_XGBoostRegressor(mocker, freqai_conf): shutil.rmtree(Path(freqai.dk.full_path)) -def test_train_model_in_series_XGBoostRegressorMultiModel(mocker, freqai_conf): +def test_extract_data_and_train_model_XGBoostRegressorMultiModel(mocker, freqai_conf): freqai_conf.update({"timerange": "20180110-20180130"}) freqai_conf.update({"freqaimodel": "XGBoostRegressorMultiTarget"}) freqai_conf.update({"strategy": "freqai_test_multimodel_strat"}) @@ -224,7 +224,8 @@ def test_train_model_in_series_XGBoostRegressorMultiModel(mocker, freqai_conf): data_load_timerange = TimeRange.parse_timerange("20180110-20180130") new_timerange = TimeRange.parse_timerange("20180120-20180130") - freqai.train_model_in_series(new_timerange, "ADA/BTC", strategy, freqai.dk, data_load_timerange) + freqai.extract_data_and_train_model( + new_timerange, "ADA/BTC", strategy, freqai.dk, data_load_timerange) assert len(freqai.dk.label_list) == 2 assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_model.joblib").is_file() From 79985fda015c0f060d0b0aca7d83962c43c75e8e Mon Sep 17 00:00:00 2001 From: Wagner Costa Santos Date: Sat, 10 Sep 2022 10:27:17 -0300 Subject: [PATCH 89/96] fix backtesting freqai startup candles bug --- freqtrade/freqai/data_kitchen.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/freqtrade/freqai/data_kitchen.py b/freqtrade/freqai/data_kitchen.py index 01b2f1f3b..88d841b9e 100644 --- a/freqtrade/freqai/data_kitchen.py +++ b/freqtrade/freqai/data_kitchen.py @@ -468,8 +468,14 @@ class FreqaiDataKitchen: Function which takes the backtesting time range and remove training data from dataframe """ + startup_candle_count = self.config.get('startup_candle_count', 0) + tf = self.config['timeframe'] tr = self.config["timerange"] + backtesting_timerange = TimeRange.parse_timerange(tr) + if startup_candle_count > 0 and backtesting_timerange: + backtesting_timerange.subtract_start(timeframe_to_seconds(tf) * startup_candle_count) + start = datetime.fromtimestamp(backtesting_timerange.startts, tz=timezone.utc) df = self.return_dataframe df = df.loc[df["date"] >= start, :] From daf352e6a5dd660e247a38a1a209ff34561ba7a3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 10 Sep 2022 16:01:06 +0200 Subject: [PATCH 90/96] Test online candle fetching in futures, too disable gateio checking on spot markets --- tests/exchange/test_ccxt_compat.py | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/tests/exchange/test_ccxt_compat.py b/tests/exchange/test_ccxt_compat.py index 49b7684f8..f57b0b366 100644 --- a/tests/exchange/test_ccxt_compat.py +++ b/tests/exchange/test_ccxt_compat.py @@ -267,13 +267,8 @@ class TestCCXTExchange(): now = datetime.now(timezone.utc) - timedelta(minutes=(timeframe_to_minutes(timeframe) * 2)) assert exchange.klines(pair_tf).iloc[-1]['date'] >= timeframe_to_prev_date(timeframe, now) - def test_ccxt__async_get_candle_history(self, exchange): - exchange, exchangename = exchange - # For some weired reason, this test returns random lengths for bittrex. - if not exchange._ft_has['ohlcv_has_history'] or exchangename == 'bittrex': - return - pair = EXCHANGES[exchangename]['pair'] - timeframe = EXCHANGES[exchangename]['timeframe'] + def ccxt__async_get_candle_history(self, exchange, exchangename, pair, timeframe): + candle_type = CandleType.SPOT timeframe_ms = timeframe_to_msecs(timeframe) now = timeframe_to_prev_date( @@ -299,6 +294,24 @@ class TestCCXTExchange(): assert len(candles) >= min(candle_count, candle_count1) assert candles[0][0] == since_ms or (since_ms + timeframe_ms) + def test_ccxt__async_get_candle_history(self, exchange): + exchange, exchangename = exchange + # For some weired reason, this test returns random lengths for bittrex. + if not exchange._ft_has['ohlcv_has_history'] or exchangename in ('bittrex', 'gateio'): + return + pair = EXCHANGES[exchangename]['pair'] + timeframe = EXCHANGES[exchangename]['timeframe'] + self.ccxt__async_get_candle_history(exchange, exchangename, pair, timeframe) + + def test_ccxt__async_get_candle_history_futures(self, exchange_futures): + exchange, exchangename = exchange_futures + if not exchange: + # exchange_futures only returns values for supported exchanges + return + pair = EXCHANGES[exchangename].get('futures_pair', EXCHANGES[exchangename]['pair']) + timeframe = EXCHANGES[exchangename]['timeframe'] + self.ccxt__async_get_candle_history(exchange, exchangename, pair, timeframe) + def test_ccxt_fetch_funding_rate_history(self, exchange_futures): exchange, exchangename = exchange_futures if not exchange: From 10b6aebc5f5ada8b1e6aef733f7c8ccf4e29e8ba Mon Sep 17 00:00:00 2001 From: robcaulk Date: Sat, 10 Sep 2022 16:54:13 +0200 Subject: [PATCH 91/96] enable continual learning and evaluation sets on multioutput models. --- .../BaseClassifierModel.py | 0 .../BaseRegressionModel.py | 0 .../BaseTensorFlowModel.py | 0 .../base_models/FreqaiMultiOutputRegressor.py | 75 +++++++++++++++++++ .../prediction_models/CatboostClassifier.py | 2 +- .../prediction_models/CatboostRegressor.py | 2 +- .../CatboostRegressorMultiTarget.py | 41 +++++++--- .../prediction_models/LightGBMClassifier.py | 2 +- .../prediction_models/LightGBMRegressor.py | 2 +- .../LightGBMRegressorMultiTarget.py | 41 +++++++--- .../prediction_models/XGBoostRegressor.py | 6 +- .../XGBoostRegressorMultiTarget.py | 37 ++++++--- 12 files changed, 170 insertions(+), 38 deletions(-) rename freqtrade/freqai/{prediction_models => base_models}/BaseClassifierModel.py (100%) rename freqtrade/freqai/{prediction_models => base_models}/BaseRegressionModel.py (100%) rename freqtrade/freqai/{prediction_models => base_models}/BaseTensorFlowModel.py (100%) create mode 100644 freqtrade/freqai/base_models/FreqaiMultiOutputRegressor.py diff --git a/freqtrade/freqai/prediction_models/BaseClassifierModel.py b/freqtrade/freqai/base_models/BaseClassifierModel.py similarity index 100% rename from freqtrade/freqai/prediction_models/BaseClassifierModel.py rename to freqtrade/freqai/base_models/BaseClassifierModel.py diff --git a/freqtrade/freqai/prediction_models/BaseRegressionModel.py b/freqtrade/freqai/base_models/BaseRegressionModel.py similarity index 100% rename from freqtrade/freqai/prediction_models/BaseRegressionModel.py rename to freqtrade/freqai/base_models/BaseRegressionModel.py diff --git a/freqtrade/freqai/prediction_models/BaseTensorFlowModel.py b/freqtrade/freqai/base_models/BaseTensorFlowModel.py similarity index 100% rename from freqtrade/freqai/prediction_models/BaseTensorFlowModel.py rename to freqtrade/freqai/base_models/BaseTensorFlowModel.py diff --git a/freqtrade/freqai/base_models/FreqaiMultiOutputRegressor.py b/freqtrade/freqai/base_models/FreqaiMultiOutputRegressor.py new file mode 100644 index 000000000..aa5dbe629 --- /dev/null +++ b/freqtrade/freqai/base_models/FreqaiMultiOutputRegressor.py @@ -0,0 +1,75 @@ + +from joblib import Parallel +from sklearn.multioutput import MultiOutputRegressor, _fit_estimator +from sklearn.utils.fixes import delayed +from sklearn.utils.validation import has_fit_parameter + + +class FreqaiMultiOutputRegressor(MultiOutputRegressor): + + def fit(self, X, y, sample_weight=None, fit_params=None): + """Fit the model to data, separately for each output variable. + Parameters + ---------- + X : {array-like, sparse matrix} of shape (n_samples, n_features) + The input data. + y : {array-like, sparse matrix} of shape (n_samples, n_outputs) + Multi-output targets. An indicator matrix turns on multilabel + estimation. + sample_weight : array-like of shape (n_samples,), default=None + Sample weights. If `None`, then samples are equally weighted. + Only supported if the underlying regressor supports sample + weights. + fit_params : A list of dicts for the fit_params + Parameters passed to the ``estimator.fit`` method of each step. + Each dict may contain same or different values (e.g. different + eval_sets or init_models) + .. versionadded:: 0.23 + Returns + ------- + self : object + Returns a fitted instance. + """ + + if not hasattr(self.estimator, "fit"): + raise ValueError("The base estimator should implement a fit method") + + y = self._validate_data(X="no_validation", y=y, multi_output=True) + + # if is_classifier(self): + # check_classification_targets(y) + + if y.ndim == 1: + raise ValueError( + "y must have at least two dimensions for " + "multi-output regression but has only one." + ) + + if sample_weight is not None and not has_fit_parameter( + self.estimator, "sample_weight" + ): + raise ValueError("Underlying estimator does not support sample weights.") + + # fit_params_validated = _check_fit_params(X, fit_params) + + if not fit_params: + fit_params = [None] * y.shape[1] + + # if not init_models: + # init_models = [None] * y.shape[1] + + self.estimators_ = Parallel(n_jobs=self.n_jobs)( + delayed(_fit_estimator)( + self.estimator, X, y[:, i], sample_weight, **fit_params[i] + # init_model=init_models[i], eval_set=eval_sets[i], + # **fit_params_validated + ) + for i in range(y.shape[1]) + ) + + if hasattr(self.estimators_[0], "n_features_in_"): + self.n_features_in_ = self.estimators_[0].n_features_in_ + if hasattr(self.estimators_[0], "feature_names_in_"): + self.feature_names_in_ = self.estimators_[0].feature_names_in_ + + return diff --git a/freqtrade/freqai/prediction_models/CatboostClassifier.py b/freqtrade/freqai/prediction_models/CatboostClassifier.py index cd7afd392..60536e6de 100644 --- a/freqtrade/freqai/prediction_models/CatboostClassifier.py +++ b/freqtrade/freqai/prediction_models/CatboostClassifier.py @@ -3,8 +3,8 @@ from typing import Any, Dict from catboost import CatBoostClassifier, Pool +from freqtrade.freqai.base_models.BaseClassifierModel import BaseClassifierModel from freqtrade.freqai.data_kitchen import FreqaiDataKitchen -from freqtrade.freqai.prediction_models.BaseClassifierModel import BaseClassifierModel logger = logging.getLogger(__name__) diff --git a/freqtrade/freqai/prediction_models/CatboostRegressor.py b/freqtrade/freqai/prediction_models/CatboostRegressor.py index 1ce31b628..73cf6c88a 100644 --- a/freqtrade/freqai/prediction_models/CatboostRegressor.py +++ b/freqtrade/freqai/prediction_models/CatboostRegressor.py @@ -3,8 +3,8 @@ from typing import Any, Dict from catboost import CatBoostRegressor, Pool +from freqtrade.freqai.base_models.BaseRegressionModel import BaseRegressionModel from freqtrade.freqai.data_kitchen import FreqaiDataKitchen -from freqtrade.freqai.prediction_models.BaseRegressionModel import BaseRegressionModel logger = logging.getLogger(__name__) diff --git a/freqtrade/freqai/prediction_models/CatboostRegressorMultiTarget.py b/freqtrade/freqai/prediction_models/CatboostRegressorMultiTarget.py index bc52bfdd9..a376b2c33 100644 --- a/freqtrade/freqai/prediction_models/CatboostRegressorMultiTarget.py +++ b/freqtrade/freqai/prediction_models/CatboostRegressorMultiTarget.py @@ -1,11 +1,11 @@ import logging from typing import Any, Dict -from catboost import CatBoostRegressor # , Pool -from sklearn.multioutput import MultiOutputRegressor +from catboost import CatBoostRegressor, Pool +from freqtrade.freqai.base_models.BaseRegressionModel import BaseRegressionModel +from freqtrade.freqai.base_models.FreqaiMultiOutputRegressor import FreqaiMultiOutputRegressor from freqtrade.freqai.data_kitchen import FreqaiDataKitchen -from freqtrade.freqai.prediction_models.BaseRegressionModel import BaseRegressionModel logger = logging.getLogger(__name__) @@ -32,17 +32,34 @@ class CatboostRegressorMultiTarget(BaseRegressionModel): X = data_dictionary["train_features"] y = data_dictionary["train_labels"] - eval_set = (data_dictionary["test_features"], data_dictionary["test_labels"]) + sample_weight = data_dictionary["train_weights"] - if self.continual_learning: - logger.warning('Continual learning not supported for MultiTarget models') - - model = MultiOutputRegressor(estimator=cbr) - model.fit(X=X, y=y, sample_weight=sample_weight) # , eval_set=eval_set) + eval_sets = [None] * y.shape[1] if self.freqai_info.get('data_split_parameters', {}).get('test_size', 0.1) != 0: - train_score = model.score(X, y) - test_score = model.score(*eval_set) - logger.info(f"Train score {train_score}, Test score {test_score}") + eval_sets = [None] * data_dictionary['test_labels'].shape[1] + + for i in range(data_dictionary['test_labels'].shape[1]): + eval_sets[i] = Pool( + data=data_dictionary["test_features"], + label=data_dictionary["test_labels"].iloc[:, i], + weight=data_dictionary["test_weights"], + ) + + init_model = self.get_init_model(dk.pair) + + if init_model: + init_models = init_model.estimators_ + else: + init_models = [None] * y.shape[1] + + fit_params = [] + for i in range(len(eval_sets)): + fit_params.append( + {'eval_set': eval_sets[i], 'init_model': init_models[i]}) + + model = FreqaiMultiOutputRegressor(estimator=cbr) + model.fit(X=X, y=y, sample_weight=sample_weight, fit_params=fit_params) + return model diff --git a/freqtrade/freqai/prediction_models/LightGBMClassifier.py b/freqtrade/freqai/prediction_models/LightGBMClassifier.py index 69867eae3..3eec516ba 100644 --- a/freqtrade/freqai/prediction_models/LightGBMClassifier.py +++ b/freqtrade/freqai/prediction_models/LightGBMClassifier.py @@ -3,8 +3,8 @@ from typing import Any, Dict from lightgbm import LGBMClassifier +from freqtrade.freqai.base_models.BaseClassifierModel import BaseClassifierModel from freqtrade.freqai.data_kitchen import FreqaiDataKitchen -from freqtrade.freqai.prediction_models.BaseClassifierModel import BaseClassifierModel logger = logging.getLogger(__name__) diff --git a/freqtrade/freqai/prediction_models/LightGBMRegressor.py b/freqtrade/freqai/prediction_models/LightGBMRegressor.py index 99e9ff887..85c9b691c 100644 --- a/freqtrade/freqai/prediction_models/LightGBMRegressor.py +++ b/freqtrade/freqai/prediction_models/LightGBMRegressor.py @@ -3,8 +3,8 @@ from typing import Any, Dict from lightgbm import LGBMRegressor +from freqtrade.freqai.base_models.BaseRegressionModel import BaseRegressionModel from freqtrade.freqai.data_kitchen import FreqaiDataKitchen -from freqtrade.freqai.prediction_models.BaseRegressionModel import BaseRegressionModel logger = logging.getLogger(__name__) diff --git a/freqtrade/freqai/prediction_models/LightGBMRegressorMultiTarget.py b/freqtrade/freqai/prediction_models/LightGBMRegressorMultiTarget.py index c34680dbe..7a9b5c36a 100644 --- a/freqtrade/freqai/prediction_models/LightGBMRegressorMultiTarget.py +++ b/freqtrade/freqai/prediction_models/LightGBMRegressorMultiTarget.py @@ -2,10 +2,10 @@ import logging from typing import Any, Dict from lightgbm import LGBMRegressor -from sklearn.multioutput import MultiOutputRegressor +from freqtrade.freqai.base_models.BaseRegressionModel import BaseRegressionModel +from freqtrade.freqai.base_models.FreqaiMultiOutputRegressor import FreqaiMultiOutputRegressor from freqtrade.freqai.data_kitchen import FreqaiDataKitchen -from freqtrade.freqai.prediction_models.BaseRegressionModel import BaseRegressionModel logger = logging.getLogger(__name__) @@ -29,15 +29,36 @@ class LightGBMRegressorMultiTarget(BaseRegressionModel): X = data_dictionary["train_features"] y = data_dictionary["train_labels"] - eval_set = (data_dictionary["test_features"], data_dictionary["test_labels"]) sample_weight = data_dictionary["train_weights"] - if self.continual_learning: - logger.warning('Continual learning not supported for MultiTarget models') + eval_weights = None + eval_sets = [None] * y.shape[1] - model = MultiOutputRegressor(estimator=lgb) - model.fit(X=X, y=y, sample_weight=sample_weight) # , eval_set=eval_set) - train_score = model.score(X, y) - test_score = model.score(*eval_set) - logger.info(f"Train score {train_score}, Test score {test_score}") + if self.freqai_info.get('data_split_parameters', {}).get('test_size', 0.1) != 0: + eval_weights = [data_dictionary["test_weights"]] + eval_sets = [(None, None)] * data_dictionary['test_labels'].shape[1] # type: ignore + for i in range(data_dictionary['test_labels'].shape[1]): + eval_sets[i] = ( # type: ignore + data_dictionary["test_features"], + data_dictionary["test_labels"].iloc[:, i] + ) + + init_model = self.get_init_model(dk.pair) + if init_model: + init_models = init_model.estimators_ + else: + init_models = [None] * y.shape[1] + + fit_params = [] + for i in range(len(eval_sets)): + fit_params.append( + {'eval_set': eval_sets[i], 'eval_sample_weight': eval_weights, + 'init_model': init_models[i]}) + + model = FreqaiMultiOutputRegressor(estimator=lgb) + model.fit(X=X, y=y, sample_weight=sample_weight, fit_params=fit_params) + + # model = FreqaiMultiOutputRegressor(estimator=lgb) + # model.fit(X=X, y=y, sample_weight=sample_weight, init_models=init_models, + # eval_sets=eval_sets, eval_sample_weight=eval_weights) return model diff --git a/freqtrade/freqai/prediction_models/XGBoostRegressor.py b/freqtrade/freqai/prediction_models/XGBoostRegressor.py index acc4386f5..c9be9ce74 100644 --- a/freqtrade/freqai/prediction_models/XGBoostRegressor.py +++ b/freqtrade/freqai/prediction_models/XGBoostRegressor.py @@ -3,8 +3,8 @@ from typing import Any, Dict from xgboost import XGBRegressor +from freqtrade.freqai.base_models.BaseRegressionModel import BaseRegressionModel from freqtrade.freqai.data_kitchen import FreqaiDataKitchen -from freqtrade.freqai.prediction_models.BaseRegressionModel import BaseRegressionModel logger = logging.getLogger(__name__) @@ -31,6 +31,7 @@ class XGBoostRegressor(BaseRegressionModel): eval_set = None else: eval_set = [(data_dictionary["test_features"], data_dictionary["test_labels"])] + eval_weights = [data_dictionary['test_weights']] sample_weight = data_dictionary["train_weights"] @@ -38,6 +39,7 @@ class XGBoostRegressor(BaseRegressionModel): model = XGBRegressor(**self.model_training_parameters) - model.fit(X=X, y=y, sample_weight=sample_weight, eval_set=eval_set, xgb_model=xgb_model) + model.fit(X=X, y=y, sample_weight=sample_weight, eval_set=eval_set, + sample_weight_eval_set=eval_weights, xgb_model=xgb_model) return model diff --git a/freqtrade/freqai/prediction_models/XGBoostRegressorMultiTarget.py b/freqtrade/freqai/prediction_models/XGBoostRegressorMultiTarget.py index 5283501d1..38c478c0b 100644 --- a/freqtrade/freqai/prediction_models/XGBoostRegressorMultiTarget.py +++ b/freqtrade/freqai/prediction_models/XGBoostRegressorMultiTarget.py @@ -1,11 +1,11 @@ import logging from typing import Any, Dict -from sklearn.multioutput import MultiOutputRegressor from xgboost import XGBRegressor +from freqtrade.freqai.base_models.BaseRegressionModel import BaseRegressionModel +from freqtrade.freqai.base_models.FreqaiMultiOutputRegressor import FreqaiMultiOutputRegressor from freqtrade.freqai.data_kitchen import FreqaiDataKitchen -from freqtrade.freqai.prediction_models.BaseRegressionModel import BaseRegressionModel logger = logging.getLogger(__name__) @@ -29,15 +29,32 @@ class XGBoostRegressorMultiTarget(BaseRegressionModel): X = data_dictionary["train_features"] y = data_dictionary["train_labels"] - eval_set = (data_dictionary["test_features"], data_dictionary["test_labels"]) sample_weight = data_dictionary["train_weights"] - if self.continual_learning: - logger.warning('Continual learning not supported for MultiTarget models') + eval_weights = None + eval_sets = [None] * y.shape[1] + + if self.freqai_info.get('data_split_parameters', {}).get('test_size', 0.1) != 0: + eval_weights = [data_dictionary["test_weights"]] + for i in range(data_dictionary['test_labels'].shape[1]): + eval_sets[i] = [( # type: ignore + data_dictionary["test_features"], + data_dictionary["test_labels"].iloc[:, i] + )] + + init_model = self.get_init_model(dk.pair) + if init_model: + init_models = init_model.estimators_ + else: + init_models = [None] * y.shape[1] + + fit_params = [] + for i in range(len(eval_sets)): + fit_params.append( + {'eval_set': eval_sets[i], 'sample_weight_eval_set': eval_weights, + 'xgb_model': init_models[i]}) + + model = FreqaiMultiOutputRegressor(estimator=xgb) + model.fit(X=X, y=y, sample_weight=sample_weight, fit_params=fit_params) - model = MultiOutputRegressor(estimator=xgb) - model.fit(X=X, y=y, sample_weight=sample_weight) # , eval_set=eval_set) - train_score = model.score(X, y) - test_score = model.score(*eval_set) - logger.info(f"Train score {train_score}, Test score {test_score}") return model From 311ae8bf1f14e4cc80d6e5ec036c942d9af716bf Mon Sep 17 00:00:00 2001 From: Wagner Costa Santos Date: Sat, 10 Sep 2022 14:45:42 -0300 Subject: [PATCH 92/96] freqai backtesting - add startup_candle_count at function description --- freqtrade/freqai/data_kitchen.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/freqai/data_kitchen.py b/freqtrade/freqai/data_kitchen.py index 88d841b9e..9168db2aa 100644 --- a/freqtrade/freqai/data_kitchen.py +++ b/freqtrade/freqai/data_kitchen.py @@ -466,7 +466,8 @@ class FreqaiDataKitchen: ) -> DataFrame: """ Function which takes the backtesting time range and - remove training data from dataframe + remove training data from dataframe, keeping only the + startup_candle_count candles """ startup_candle_count = self.config.get('startup_candle_count', 0) tf = self.config['timeframe'] From f97f1dc5c38bd7e3c0b731404040a2a8e56a20e5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 10 Sep 2022 19:57:21 +0200 Subject: [PATCH 93/96] Test CatboostRegressorMultiTarget, simplify test setup via parametrization --- tests/freqai/test_freqai_interface.py | 44 ++++++--------------------- 1 file changed, 10 insertions(+), 34 deletions(-) diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index 5f8eeb086..115ad0ea8 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -46,10 +46,18 @@ def test_extract_data_and_train_model_LightGBM(mocker, freqai_conf): shutil.rmtree(Path(freqai.dk.full_path)) -def test_extract_data_and_train_model_LightGBMMultiModel(mocker, freqai_conf): +@pytest.mark.parametrize('model', [ + 'LightGBMRegressorMultiTarget', + 'XGBoostRegressorMultiTarget', + 'CatboostRegressorMultiTarget', + ]) +def test_extract_data_and_train_model_MultiTargets(mocker, freqai_conf, model): + if is_arm() and model == 'CatboostRegressorMultiTarget': + pytest.skip("CatBoost is not supported on ARM") + freqai_conf.update({"timerange": "20180110-20180130"}) freqai_conf.update({"strategy": "freqai_test_multimodel_strat"}) - freqai_conf.update({"freqaimodel": "LightGBMRegressorMultiTarget"}) + freqai_conf.update({"freqaimodel": model}) strategy = get_patched_freqai_strategy(mocker, freqai_conf) exchange = get_patched_exchange(mocker, freqai_conf) strategy.dp = DataProvider(freqai_conf, exchange) @@ -205,38 +213,6 @@ def test_extract_data_and_train_model_XGBoostRegressor(mocker, freqai_conf): shutil.rmtree(Path(freqai.dk.full_path)) -def test_extract_data_and_train_model_XGBoostRegressorMultiModel(mocker, freqai_conf): - freqai_conf.update({"timerange": "20180110-20180130"}) - freqai_conf.update({"freqaimodel": "XGBoostRegressorMultiTarget"}) - freqai_conf.update({"strategy": "freqai_test_multimodel_strat"}) - strategy = get_patched_freqai_strategy(mocker, freqai_conf) - exchange = get_patched_exchange(mocker, freqai_conf) - strategy.dp = DataProvider(freqai_conf, exchange) - strategy.freqai_info = freqai_conf.get("freqai", {}) - freqai = strategy.freqai - freqai.live = True - freqai.dk = FreqaiDataKitchen(freqai_conf) - timerange = TimeRange.parse_timerange("20180110-20180130") - freqai.dd.load_all_pair_histories(timerange, freqai.dk) - - freqai.dd.pair_dict = MagicMock() - - data_load_timerange = TimeRange.parse_timerange("20180110-20180130") - new_timerange = TimeRange.parse_timerange("20180120-20180130") - - freqai.extract_data_and_train_model( - new_timerange, "ADA/BTC", strategy, freqai.dk, data_load_timerange) - - assert len(freqai.dk.label_list) == 2 - assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_model.joblib").is_file() - assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_metadata.json").is_file() - assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_trained_df.pkl").is_file() - assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_svm_model.joblib").is_file() - assert len(freqai.dk.data['training_features_list']) == 26 - - shutil.rmtree(Path(freqai.dk.full_path)) - - def test_start_backtesting(mocker, freqai_conf): freqai_conf.update({"timerange": "20180120-20180130"}) freqai_conf.get("freqai", {}).update({"save_backtest_models": True}) From 88892ba663ea8805e06ecd2d101c8982f595ad5f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 10 Sep 2022 20:06:52 +0200 Subject: [PATCH 94/96] Parametrize regressor tests --- tests/freqai/test_freqai_interface.py | 76 ++++----------------------- 1 file changed, 11 insertions(+), 65 deletions(-) diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index 115ad0ea8..bd921c16b 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -17,8 +17,18 @@ def is_arm() -> bool: return "arm" in machine or "aarch64" in machine -def test_extract_data_and_train_model_LightGBM(mocker, freqai_conf): +@pytest.mark.parametrize('model', [ + 'LightGBMRegressor', + 'XGBoostRegressor', + 'CatboostRegressor', + ]) +def test_extract_data_and_train_model_Regressors(mocker, freqai_conf, model): + if is_arm() and model == 'CatboostRegressor': + pytest.skip("CatBoost is not supported on ARM") + + freqai_conf.update({"freqaimodel": model}) freqai_conf.update({"timerange": "20180110-20180130"}) + freqai_conf.update({"strategy": "freqai_test_strat"}) strategy = get_patched_freqai_strategy(mocker, freqai_conf) exchange = get_patched_exchange(mocker, freqai_conf) @@ -86,39 +96,6 @@ def test_extract_data_and_train_model_MultiTargets(mocker, freqai_conf, model): shutil.rmtree(Path(freqai.dk.full_path)) -@pytest.mark.skipif(is_arm(), reason="no ARM for Catboost ...") -def test_extract_data_and_train_model_Catboost(mocker, freqai_conf): - freqai_conf.update({"timerange": "20180110-20180130"}) - freqai_conf.update({"freqaimodel": "CatboostRegressor"}) - # freqai_conf.get('freqai', {}).update( - # {'model_training_parameters': {"n_estimators": 100, "verbose": 0}}) - strategy = get_patched_freqai_strategy(mocker, freqai_conf) - exchange = get_patched_exchange(mocker, freqai_conf) - strategy.dp = DataProvider(freqai_conf, exchange) - - strategy.freqai_info = freqai_conf.get("freqai", {}) - freqai = strategy.freqai - freqai.live = True - freqai.dk = FreqaiDataKitchen(freqai_conf) - timerange = TimeRange.parse_timerange("20180110-20180130") - freqai.dd.load_all_pair_histories(timerange, freqai.dk) - - freqai.dd.pair_dict = MagicMock() - - data_load_timerange = TimeRange.parse_timerange("20180110-20180130") - new_timerange = TimeRange.parse_timerange("20180120-20180130") - - freqai.extract_data_and_train_model(new_timerange, "ADA/BTC", - strategy, freqai.dk, data_load_timerange) - - assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_model.joblib").exists() - assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_metadata.json").exists() - assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_trained_df.pkl").exists() - assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_svm_model.joblib").exists() - - shutil.rmtree(Path(freqai.dk.full_path)) - - @pytest.mark.skipif(is_arm(), reason="no ARM for Catboost ...") def test_extract_data_and_train_model_CatboostClassifier(mocker, freqai_conf): freqai_conf.update({"timerange": "20180110-20180130"}) @@ -182,37 +159,6 @@ def test_extract_data_and_train_model_LightGBMClassifier(mocker, freqai_conf): shutil.rmtree(Path(freqai.dk.full_path)) -def test_extract_data_and_train_model_XGBoostRegressor(mocker, freqai_conf): - freqai_conf.update({"timerange": "20180110-20180130"}) - freqai_conf.update({"freqaimodel": "XGBoostRegressor"}) - freqai_conf.update({"strategy": "freqai_test_strat"}) - - strategy = get_patched_freqai_strategy(mocker, freqai_conf) - exchange = get_patched_exchange(mocker, freqai_conf) - strategy.dp = DataProvider(freqai_conf, exchange) - strategy.freqai_info = freqai_conf.get("freqai", {}) - freqai = strategy.freqai - freqai.live = True - freqai.dk = FreqaiDataKitchen(freqai_conf) - timerange = TimeRange.parse_timerange("20180110-20180130") - freqai.dd.load_all_pair_histories(timerange, freqai.dk) - - freqai.dd.pair_dict = MagicMock() - - data_load_timerange = TimeRange.parse_timerange("20180110-20180130") - new_timerange = TimeRange.parse_timerange("20180120-20180130") - - freqai.extract_data_and_train_model( - new_timerange, "ADA/BTC", strategy, freqai.dk, data_load_timerange) - - assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_model.joblib").is_file() - assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_metadata.json").is_file() - assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_trained_df.pkl").is_file() - assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_svm_model.joblib").is_file() - - shutil.rmtree(Path(freqai.dk.full_path)) - - def test_start_backtesting(mocker, freqai_conf): freqai_conf.update({"timerange": "20180120-20180130"}) freqai_conf.get("freqai", {}).update({"save_backtest_models": True}) From b3fc1cfde94b9b8e67e79d1f35f4ce3f36a4e5e0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 10 Sep 2022 20:17:57 +0200 Subject: [PATCH 95/96] Parametrize classifier tests --- tests/freqai/test_freqai_interface.py | 43 ++++++--------------------- 1 file changed, 9 insertions(+), 34 deletions(-) diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index bd921c16b..2a7cfeb73 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -96,42 +96,17 @@ def test_extract_data_and_train_model_MultiTargets(mocker, freqai_conf, model): shutil.rmtree(Path(freqai.dk.full_path)) -@pytest.mark.skipif(is_arm(), reason="no ARM for Catboost ...") -def test_extract_data_and_train_model_CatboostClassifier(mocker, freqai_conf): - freqai_conf.update({"timerange": "20180110-20180130"}) - freqai_conf.update({"freqaimodel": "CatboostClassifier"}) +@pytest.mark.parametrize('model', [ + 'LightGBMClassifier', + 'CatboostClassifier', + ]) +def test_extract_data_and_train_model_Classifiers(mocker, freqai_conf, model): + if is_arm() and model == 'CatboostClassifier': + pytest.skip("CatBoost is not supported on ARM") + + freqai_conf.update({"freqaimodel": model}) freqai_conf.update({"strategy": "freqai_test_classifier"}) - strategy = get_patched_freqai_strategy(mocker, freqai_conf) - exchange = get_patched_exchange(mocker, freqai_conf) - strategy.dp = DataProvider(freqai_conf, exchange) - - strategy.freqai_info = freqai_conf.get("freqai", {}) - freqai = strategy.freqai - freqai.live = True - freqai.dk = FreqaiDataKitchen(freqai_conf) - timerange = TimeRange.parse_timerange("20180110-20180130") - freqai.dd.load_all_pair_histories(timerange, freqai.dk) - - freqai.dd.pair_dict = MagicMock() - - data_load_timerange = TimeRange.parse_timerange("20180110-20180130") - new_timerange = TimeRange.parse_timerange("20180120-20180130") - - freqai.extract_data_and_train_model(new_timerange, "ADA/BTC", - strategy, freqai.dk, data_load_timerange) - - assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_model.joblib").exists() - assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_metadata.json").exists() - assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_trained_df.pkl").exists() - assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_svm_model.joblib").exists() - - shutil.rmtree(Path(freqai.dk.full_path)) - - -def test_extract_data_and_train_model_LightGBMClassifier(mocker, freqai_conf): freqai_conf.update({"timerange": "20180110-20180130"}) - freqai_conf.update({"freqaimodel": "LightGBMClassifier"}) - freqai_conf.update({"strategy": "freqai_test_classifier"}) strategy = get_patched_freqai_strategy(mocker, freqai_conf) exchange = get_patched_exchange(mocker, freqai_conf) strategy.dp = DataProvider(freqai_conf, exchange) From 5a0cfee27e8de330d00168b6de86a2ac84e0ed7f Mon Sep 17 00:00:00 2001 From: robcaulk Date: Sat, 10 Sep 2022 22:16:49 +0200 Subject: [PATCH 96/96] allow user to multithread jobs (advanced users only) --- .../freqai/base_models/FreqaiMultiOutputRegressor.py | 10 ---------- .../prediction_models/CatboostRegressorMultiTarget.py | 3 +++ .../prediction_models/LightGBMRegressorMultiTarget.py | 6 +++--- .../prediction_models/XGBoostRegressorMultiTarget.py | 3 +++ 4 files changed, 9 insertions(+), 13 deletions(-) diff --git a/freqtrade/freqai/base_models/FreqaiMultiOutputRegressor.py b/freqtrade/freqai/base_models/FreqaiMultiOutputRegressor.py index aa5dbe629..a9db81e31 100644 --- a/freqtrade/freqai/base_models/FreqaiMultiOutputRegressor.py +++ b/freqtrade/freqai/base_models/FreqaiMultiOutputRegressor.py @@ -36,9 +36,6 @@ class FreqaiMultiOutputRegressor(MultiOutputRegressor): y = self._validate_data(X="no_validation", y=y, multi_output=True) - # if is_classifier(self): - # check_classification_targets(y) - if y.ndim == 1: raise ValueError( "y must have at least two dimensions for " @@ -50,19 +47,12 @@ class FreqaiMultiOutputRegressor(MultiOutputRegressor): ): raise ValueError("Underlying estimator does not support sample weights.") - # fit_params_validated = _check_fit_params(X, fit_params) - if not fit_params: fit_params = [None] * y.shape[1] - # if not init_models: - # init_models = [None] * y.shape[1] - self.estimators_ = Parallel(n_jobs=self.n_jobs)( delayed(_fit_estimator)( self.estimator, X, y[:, i], sample_weight, **fit_params[i] - # init_model=init_models[i], eval_set=eval_sets[i], - # **fit_params_validated ) for i in range(y.shape[1]) ) diff --git a/freqtrade/freqai/prediction_models/CatboostRegressorMultiTarget.py b/freqtrade/freqai/prediction_models/CatboostRegressorMultiTarget.py index a376b2c33..7fa4e293e 100644 --- a/freqtrade/freqai/prediction_models/CatboostRegressorMultiTarget.py +++ b/freqtrade/freqai/prediction_models/CatboostRegressorMultiTarget.py @@ -60,6 +60,9 @@ class CatboostRegressorMultiTarget(BaseRegressionModel): {'eval_set': eval_sets[i], 'init_model': init_models[i]}) model = FreqaiMultiOutputRegressor(estimator=cbr) + thread_training = self.freqai_info.get('multitarget_parallel_training', False) + if thread_training: + model.n_jobs = y.shape[1] model.fit(X=X, y=y, sample_weight=sample_weight, fit_params=fit_params) return model diff --git a/freqtrade/freqai/prediction_models/LightGBMRegressorMultiTarget.py b/freqtrade/freqai/prediction_models/LightGBMRegressorMultiTarget.py index 7a9b5c36a..37c6bb186 100644 --- a/freqtrade/freqai/prediction_models/LightGBMRegressorMultiTarget.py +++ b/freqtrade/freqai/prediction_models/LightGBMRegressorMultiTarget.py @@ -56,9 +56,9 @@ class LightGBMRegressorMultiTarget(BaseRegressionModel): 'init_model': init_models[i]}) model = FreqaiMultiOutputRegressor(estimator=lgb) + thread_training = self.freqai_info.get('multitarget_parallel_training', False) + if thread_training: + model.n_jobs = y.shape[1] model.fit(X=X, y=y, sample_weight=sample_weight, fit_params=fit_params) - # model = FreqaiMultiOutputRegressor(estimator=lgb) - # model.fit(X=X, y=y, sample_weight=sample_weight, init_models=init_models, - # eval_sets=eval_sets, eval_sample_weight=eval_weights) return model diff --git a/freqtrade/freqai/prediction_models/XGBoostRegressorMultiTarget.py b/freqtrade/freqai/prediction_models/XGBoostRegressorMultiTarget.py index 38c478c0b..920745ec9 100644 --- a/freqtrade/freqai/prediction_models/XGBoostRegressorMultiTarget.py +++ b/freqtrade/freqai/prediction_models/XGBoostRegressorMultiTarget.py @@ -55,6 +55,9 @@ class XGBoostRegressorMultiTarget(BaseRegressionModel): 'xgb_model': init_models[i]}) model = FreqaiMultiOutputRegressor(estimator=xgb) + thread_training = self.freqai_info.get('multitarget_parallel_training', False) + if thread_training: + model.n_jobs = y.shape[1] model.fit(X=X, y=y, sample_weight=sample_weight, fit_params=fit_params) return model