From 769bd44d356244725bbd9a8656b4557b8b711140 Mon Sep 17 00:00:00 2001 From: longyu Date: Mon, 4 Jul 2022 09:01:29 +0200 Subject: [PATCH 001/126] change the std factor --- freqtrade/templates/FreqaiExampleStrategy.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/templates/FreqaiExampleStrategy.py b/freqtrade/templates/FreqaiExampleStrategy.py index f70277c77..5d3a1dd05 100644 --- a/freqtrade/templates/FreqaiExampleStrategy.py +++ b/freqtrade/templates/FreqaiExampleStrategy.py @@ -195,8 +195,8 @@ class FreqaiExampleStrategy(IStrategy): # each training period. dataframe = self.model.bridge.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 + dataframe["target_roi"] = dataframe["&-s_close_mean"] + dataframe["&-s_close_std"] * 1.1 + dataframe["sell_roi"] = dataframe["&-s_close_mean"] - dataframe["&-s_close_std"] * 1.1 return dataframe def populate_entry_trend(self, df: DataFrame, metadata: dict) -> DataFrame: From 7c850a4c2d688624977f8a00e13cf8e98111c1a4 Mon Sep 17 00:00:00 2001 From: longyu Date: Mon, 11 Jul 2022 22:53:35 +0200 Subject: [PATCH 002/126] test the score during the training in the multi model --- .../prediction_models/CatboostPredictionMultiModel.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqai/prediction_models/CatboostPredictionMultiModel.py b/freqtrade/freqai/prediction_models/CatboostPredictionMultiModel.py index c4d92d7bb..5861d9bf8 100644 --- a/freqtrade/freqai/prediction_models/CatboostPredictionMultiModel.py +++ b/freqtrade/freqai/prediction_models/CatboostPredictionMultiModel.py @@ -90,12 +90,14 @@ class CatboostPredictionMultiModel(IFreqaiModel): X = data_dictionary["train_features"] y = data_dictionary["train_labels"] - # eval_set = (data_dictionary["test_features"], data_dictionary["test_labels"]) + eval_set = (data_dictionary["test_features"], data_dictionary["test_labels"]) sample_weight = data_dictionary["train_weights"] model = MultiOutputRegressor(estimator=cbr) 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 def predict( From 3fefa4b144243c3534c42deace3228571647cef3 Mon Sep 17 00:00:00 2001 From: longyu Date: Mon, 11 Jul 2022 23:36:01 +0200 Subject: [PATCH 003/126] add shuffle parameter explaination --- docs/freqai.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/freqai.md b/docs/freqai.md index a0a11ac35..a1b00c75c 100644 --- a/docs/freqai.md +++ b/docs/freqai.md @@ -408,6 +408,11 @@ It is common to want constant retraining, in whichcase, user should set `live_re ### Controlling the model learning process +Depending on what AI model to be used, these parameter names could be different. For example, the accepted parameters for the `Catboost` +models are `data_split_parameters`, `n_estimators` and etc. For the model like SVM regression model, the accepted parameters are different. + +Here we explan the parameters of `model_training_parameters` for `Catboost`: + The user can define model settings for the data split `data_split_parameters` and learning parameters `model_training_parameters`. Users are encouraged to visit the Catboost documentation for more information on how to select these values. `n_estimators` increases the @@ -425,6 +430,8 @@ where $W_i$ is the weight of data point $i$ in a total set of $n$ data points._ Finally, `period` defines the offset used for the `labels`. In the present example, the user is asking for `labels` that are 24 candles in the future. +Note: since we work time series data and want to train a AI model to predict the future, the validation/test data should be the "future" by a given training data. Thus, we strongly recommend to disable `shuffle` parameter during the cross-validation steps. For more detailed explaination, visit [here](https://medium.com/@soumyachess1496/cross-validation-in-time-series-566ae4981ce4). + ### Removing outliers with the Dissimilarity Index The Dissimilarity Index (DI) aims to quantify the uncertainty associated with each From da9db8eaa25a31a26dca18354c6102f4a9a19217 Mon Sep 17 00:00:00 2001 From: longyu Date: Mon, 11 Jul 2022 23:43:57 +0200 Subject: [PATCH 004/126] undo 1.25 --- freqtrade/templates/FreqaiExampleStrategy.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/templates/FreqaiExampleStrategy.py b/freqtrade/templates/FreqaiExampleStrategy.py index 4fe5de5ef..ca08b8168 100644 --- a/freqtrade/templates/FreqaiExampleStrategy.py +++ b/freqtrade/templates/FreqaiExampleStrategy.py @@ -199,8 +199,8 @@ class FreqaiExampleStrategy(IStrategy): # each training period. dataframe = self.model.bridge.start(dataframe, metadata, self) - dataframe["target_roi"] = dataframe["&-s_close_mean"] + dataframe["&-s_close_std"] * 1.1 - dataframe["sell_roi"] = dataframe["&-s_close_mean"] - dataframe["&-s_close_std"] * 1.1 + 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 return dataframe def populate_entry_trend(self, df: DataFrame, metadata: dict) -> DataFrame: From 7b6397b08f13bc99f90e80b401130413d24a79a6 Mon Sep 17 00:00:00 2001 From: longyu Date: Wed, 13 Jul 2022 19:00:54 +0200 Subject: [PATCH 005/126] fix indicator_max_period parameter --- 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 56c1a67ed..f320bdc2f 100644 --- a/freqtrade/freqai/data_kitchen.py +++ b/freqtrade/freqai/data_kitchen.py @@ -799,7 +799,7 @@ class FreqaiDataKitchen: max_timeframe_chars = self.freqai_config.get( "feature_parameters", {}).get("include_timeframes")[-1] max_period = self.freqai_config.get("feature_parameters", {}).get( - "indicator_max_period", 50 + "indicator_max_period_candles", 50 ) additional_seconds = 0 if max_timeframe_chars[-1] == "d": From a6e1535ad8e8da1cd4cad82474c1c07a7105c006 Mon Sep 17 00:00:00 2001 From: longyu Date: Wed, 13 Jul 2022 19:12:39 +0200 Subject: [PATCH 006/126] clean a bit of doc --- docs/freqai.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/freqai.md b/docs/freqai.md index a1b00c75c..4df6ea730 100644 --- a/docs/freqai.md +++ b/docs/freqai.md @@ -409,7 +409,7 @@ It is common to want constant retraining, in whichcase, user should set `live_re ### Controlling the model learning process Depending on what AI model to be used, these parameter names could be different. For example, the accepted parameters for the `Catboost` -models are `data_split_parameters`, `n_estimators` and etc. For the model like SVM regression model, the accepted parameters are different. +models are `n_estimators`, `task_type` and others. For the model like SVM regression model, the accepted parameters are different. Here we explan the parameters of `model_training_parameters` for `Catboost`: From 44650ed8a90bbdde204da99b0a6a38fbee1950a5 Mon Sep 17 00:00:00 2001 From: longyu Date: Wed, 13 Jul 2022 19:14:10 +0200 Subject: [PATCH 007/126] clean a bit of doc --- docs/freqai.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/freqai.md b/docs/freqai.md index 4df6ea730..9b01cbc13 100644 --- a/docs/freqai.md +++ b/docs/freqai.md @@ -430,7 +430,7 @@ where $W_i$ is the weight of data point $i$ in a total set of $n$ data points._ Finally, `period` defines the offset used for the `labels`. In the present example, the user is asking for `labels` that are 24 candles in the future. -Note: since we work time series data and want to train a AI model to predict the future, the validation/test data should be the "future" by a given training data. Thus, we strongly recommend to disable `shuffle` parameter during the cross-validation steps. For more detailed explaination, visit [here](https://medium.com/@soumyachess1496/cross-validation-in-time-series-566ae4981ce4). +Note: typically in time-series forecasting, the validation/test data should be the "future" by a given training data. Thus, it is nicely to disable `shuffle` and `data_split_parameters` parameters during the cross-validation or validation steps. For more detailed explaination, visit [here](https://medium.com/@soumyachess1496/cross-validation-in-time-series-566ae4981ce4). ### Removing outliers with the Dissimilarity Index From 00461850f52bf932885afac1be6ac5ef818cd6c6 Mon Sep 17 00:00:00 2001 From: longyu Date: Wed, 13 Jul 2022 19:18:13 +0200 Subject: [PATCH 008/126] change doc --- docs/freqai.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/freqai.md b/docs/freqai.md index 9b01cbc13..9699ff168 100644 --- a/docs/freqai.md +++ b/docs/freqai.md @@ -430,7 +430,7 @@ where $W_i$ is the weight of data point $i$ in a total set of $n$ data points._ Finally, `period` defines the offset used for the `labels`. In the present example, the user is asking for `labels` that are 24 candles in the future. -Note: typically in time-series forecasting, the validation/test data should be the "future" by a given training data. Thus, it is nicely to disable `shuffle` and `data_split_parameters` parameters during the cross-validation or validation steps. For more detailed explaination, visit [here](https://medium.com/@soumyachess1496/cross-validation-in-time-series-566ae4981ce4). +Note: typically in time-series forecasting, the validation/test data should be the "future" by a given training data. Thus, it is recommended to disable `shuffle` parameter during the cross-validation or validation steps. For more detailed explaination, visit [here](https://medium.com/@soumyachess1496/cross-validation-in-time-series-566ae4981ce4). ### Removing outliers with the Dissimilarity Index From 7260a71ecbcd55b6bc4f377f5a750df10ecb7e69 Mon Sep 17 00:00:00 2001 From: longyu Date: Fri, 15 Jul 2022 23:02:09 +0200 Subject: [PATCH 009/126] refactor set_weights_higher_recent --- freqtrade/freqai/data_kitchen.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/freqtrade/freqai/data_kitchen.py b/freqtrade/freqai/data_kitchen.py index f320bdc2f..74d763e1b 100644 --- a/freqtrade/freqai/data_kitchen.py +++ b/freqtrade/freqai/data_kitchen.py @@ -675,12 +675,10 @@ class FreqaiDataKitchen: Set weights so that recent data is more heavily weighted during training than older data. """ - + wfactor = self.config["freqai"]["feature_parameters"]["weight_factor"] weights = np.zeros(num_weights) - for i in range(1, len(weights)): - weights[len(weights) - i] = np.exp( - -i / (self.config["freqai"]["feature_parameters"]["weight_factor"] * num_weights) - ) + weights[1:] = np.exp( + - np.arange(1, len(weights)) / (wfactor * num_weights))[::-1] return weights def append_predictions(self, predictions, do_predict, len_dataframe): From 2fdea6d43a35a82b282fe058b11b78a28bb68435 Mon Sep 17 00:00:00 2001 From: longyu Date: Sat, 16 Jul 2022 15:02:30 +0200 Subject: [PATCH 010/126] print start and end date of the train/test data --- freqtrade/freqai/prediction_models/BaseRegressionModel.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/freqtrade/freqai/prediction_models/BaseRegressionModel.py b/freqtrade/freqai/prediction_models/BaseRegressionModel.py index f9a9bb69f..6f345ef67 100644 --- a/freqtrade/freqai/prediction_models/BaseRegressionModel.py +++ b/freqtrade/freqai/prediction_models/BaseRegressionModel.py @@ -39,7 +39,10 @@ class BaseRegressionModel(IFreqaiModel): :model: Trained model which can be used to inference (self.predict) """ - logger.info("--------------------Starting training " f"{pair} --------------------") + start_date = unfiltered_dataframe["date"].iloc[0] + end_date = unfiltered_dataframe["date"].iloc[-1] + logger.info("-------------------- Starting training " f"{pair} --------------------") + logger.info("-------------------- Using data " f"from {start_date} to {end_date}--------------------") # filter the features requested by user in the configuration file and elegantly handle NaNs features_filtered, labels_filtered = dk.filter_features( From 56ad107769998a19b29056eba3543880b13112eb Mon Sep 17 00:00:00 2001 From: robcaulk Date: Sun, 17 Jul 2022 20:58:26 +0200 Subject: [PATCH 011/126] Fix english, generalize writing, improve clarity --- docs/freqai.md | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/docs/freqai.md b/docs/freqai.md index 9699ff168..a5d7458f5 100644 --- a/docs/freqai.md +++ b/docs/freqai.md @@ -408,17 +408,9 @@ It is common to want constant retraining, in whichcase, user should set `live_re ### Controlling the model learning process -Depending on what AI model to be used, these parameter names could be different. For example, the accepted parameters for the `Catboost` -models are `n_estimators`, `task_type` and others. For the model like SVM regression model, the accepted parameters are different. +Model training parameters are unqiue to the library employed by the user. FreqAI allows users to set any parameter for any library using the `model_training_parameters` dictionary in the user configuration file. The example configuration files show some of the example parameters associated with `Catboost` and `LightGBM`, but users can add any parameters available in those libraries. -Here we explan the parameters of `model_training_parameters` for `Catboost`: - -The user can define model settings for the data split `data_split_parameters` and learning parameters -`model_training_parameters`. Users are encouraged to visit the Catboost documentation -for more information on how to select these values. `n_estimators` increases the -computational effort and the fit to the training data. If a user has a GPU -installed in their system, they may benefit from changing `task_type` to `GPU`. -The `weight_factor` allows the user to weight more recent data more strongly +Data split parameters are defined in `data_split_parameters` which can be any parameters associated with `Sklearn`'s `train_test_split()` function. Meanwhile, FreqAI includes some additional parameters such `weight_factor` which allows the user to weight more recent data more strongly than past data via an exponential function: $$ W_i = \exp(\frac{-i}{\alpha*n}) $$ @@ -427,11 +419,11 @@ where $W_i$ is the weight of data point $i$ in a total set of $n$ data points._ ![weight-factor](assets/weights_factor.png) -Finally, `period` defines the offset used for the `labels`. In the present example, +`train_test_split()` has a parameters called `shuffle`, which users also have access to in FreqAI, that allows them to keep the data unshuffled. This is particularly useful to avoid biasing training with temporally autocorrelated data. + +Finally, `label_period_candles` defines the offset used for the `labels`. In the present example, the user is asking for `labels` that are 24 candles in the future. -Note: typically in time-series forecasting, the validation/test data should be the "future" by a given training data. Thus, it is recommended to disable `shuffle` parameter during the cross-validation or validation steps. For more detailed explaination, visit [here](https://medium.com/@soumyachess1496/cross-validation-in-time-series-566ae4981ce4). - ### Removing outliers with the Dissimilarity Index The Dissimilarity Index (DI) aims to quantify the uncertainty associated with each From d52073d5469894d8b3d553894daf1d415170143b Mon Sep 17 00:00:00 2001 From: longyu Date: Tue, 19 Jul 2022 09:39:36 +0200 Subject: [PATCH 012/126] too long line fix --- freqtrade/freqai/prediction_models/BaseRegressionModel.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/freqai/prediction_models/BaseRegressionModel.py b/freqtrade/freqai/prediction_models/BaseRegressionModel.py index 6f345ef67..63a589e1f 100644 --- a/freqtrade/freqai/prediction_models/BaseRegressionModel.py +++ b/freqtrade/freqai/prediction_models/BaseRegressionModel.py @@ -42,7 +42,8 @@ class BaseRegressionModel(IFreqaiModel): start_date = unfiltered_dataframe["date"].iloc[0] end_date = unfiltered_dataframe["date"].iloc[-1] logger.info("-------------------- Starting training " f"{pair} --------------------") - logger.info("-------------------- Using data " f"from {start_date} to {end_date}--------------------") + logger.info("-------------------- Using data " + f"from {start_date} to {end_date}--------------------") # filter the features requested by user in the configuration file and elegantly handle NaNs features_filtered, labels_filtered = dk.filter_features( From 1b555ba46250e63fd71653d10ba53eb972e213de Mon Sep 17 00:00:00 2001 From: robcaulk Date: Tue, 19 Jul 2022 17:28:38 +0200 Subject: [PATCH 013/126] remove unnecessary definition of np array --- freqtrade/freqai/data_kitchen.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/freqtrade/freqai/data_kitchen.py b/freqtrade/freqai/data_kitchen.py index 74d763e1b..8050b8b3b 100644 --- a/freqtrade/freqai/data_kitchen.py +++ b/freqtrade/freqai/data_kitchen.py @@ -676,9 +676,8 @@ class FreqaiDataKitchen: training than older data. """ wfactor = self.config["freqai"]["feature_parameters"]["weight_factor"] - weights = np.zeros(num_weights) - weights[1:] = np.exp( - - np.arange(1, len(weights)) / (wfactor * num_weights))[::-1] + weights = np.exp( + - np.arange(num_weights) / (wfactor * num_weights))[::-1] return weights def append_predictions(self, predictions, do_predict, len_dataframe): From 5ad8d08b8453a65fc55cad708f497df98c6a1f5c Mon Sep 17 00:00:00 2001 From: robcaulk Date: Tue, 19 Jul 2022 17:45:12 +0200 Subject: [PATCH 014/126] logged dates for filtered dataframe instead of unfiltered dataframe --- freqtrade/freqai/prediction_models/BaseRegressionModel.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/freqai/prediction_models/BaseRegressionModel.py b/freqtrade/freqai/prediction_models/BaseRegressionModel.py index 63a589e1f..ffe30ef2a 100644 --- a/freqtrade/freqai/prediction_models/BaseRegressionModel.py +++ b/freqtrade/freqai/prediction_models/BaseRegressionModel.py @@ -39,11 +39,7 @@ class BaseRegressionModel(IFreqaiModel): :model: Trained model which can be used to inference (self.predict) """ - start_date = unfiltered_dataframe["date"].iloc[0] - end_date = unfiltered_dataframe["date"].iloc[-1] logger.info("-------------------- Starting training " f"{pair} --------------------") - logger.info("-------------------- Using data " - f"from {start_date} to {end_date}--------------------") # filter the features requested by user in the configuration file and elegantly handle NaNs features_filtered, labels_filtered = dk.filter_features( @@ -53,6 +49,10 @@ class BaseRegressionModel(IFreqaiModel): 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") + logger.info(f"-------------------- Training on data from {start_date} to " + f"{end_date}--------------------") # split data into train/test data. data_dictionary = dk.make_train_test_datasets(features_filtered, labels_filtered) if not self.freqai_info.get('fit_live_predictions', 0): From 38841e30b8ebebaa4c1fa7ed10a804cec79c03f8 Mon Sep 17 00:00:00 2001 From: longyu Date: Fri, 22 Jul 2022 17:17:57 +0200 Subject: [PATCH 015/126] fix conflicts --- README.md | 1 + docs/freqai.md | 125 ++++++++----- freqtrade/freqai/data_drawer.py | 2 +- freqtrade/freqai/data_kitchen.py | 165 +++++++---------- freqtrade/freqai/freqai_interface.py | 25 ++- freqtrade/templates/FreqaiExampleStrategy.py | 29 +-- mkdocs.yml | 2 +- requirements-dev.txt | 1 + requirements-freqai.txt | 2 +- setup.sh | 10 +- tests/freqai/conftest.py | 117 ++++++++++++ tests/freqai/test_freqai_datakitchen.py | 167 +++++++++++++++++ tests/freqai/test_freqai_interface.py | 181 ++++++++++++++++++ tests/rpc/test_rpc_apiserver.py | 3 +- tests/strategy/strats/freqai_test_strat.py | 182 +++++++++++++++++++ tests/strategy/test_strategy_loading.py | 6 +- 16 files changed, 838 insertions(+), 180 deletions(-) create mode 100644 tests/freqai/conftest.py create mode 100644 tests/freqai/test_freqai_datakitchen.py create mode 100644 tests/freqai/test_freqai_interface.py create mode 100644 tests/strategy/strats/freqai_test_strat.py diff --git a/README.md b/README.md index 881895c9a..1c75652be 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,7 @@ Please find the complete documentation on the [freqtrade website](https://www.fr - [x] **Dry-run**: Run the bot without paying money. - [x] **Backtesting**: Run a simulation of your buy/sell strategy. - [x] **Strategy Optimization by machine learning**: Use machine learning to optimize your buy/sell strategy parameters with real exchange data. +- [X] **Adaptive prediction modeling**: Build a smart strategy with FreqAI that self-trains to the market via adaptive machine learning methods. [Learn more](https://www.freqtrade.io/en/stable/freqai/) - [x] **Edge position sizing** Calculate your win rate, risk reward ratio, the best stoploss and adjust your position size before taking a position for each specific market. [Learn more](https://www.freqtrade.io/en/stable/edge/). - [x] **Whitelist crypto-currencies**: Select which crypto-currency you want to trade or use dynamic whitelists. - [x] **Blacklist crypto-currencies**: Select which crypto-currency you want to avoid. diff --git a/docs/freqai.md b/docs/freqai.md index a5d7458f5..aa53cac6b 100644 --- a/docs/freqai.md +++ b/docs/freqai.md @@ -1,30 +1,26 @@ -# Freqai +# FreqAI -!!! Note - Freqai is still experimental, and should be used at the user's own discretion. - -Freqai is a module designed to automate a variety of tasks associated with +FreqAI is a module designed to automate a variety of tasks associated with training a predictive model to provide signals based on input features. Among the the features included: -* Easy large feature set construction based on simple user input -* Sweep model training and backtesting to simulate consistent model retraining through time -* Smart outlier removal of data points from prediction sets using a Dissimilarity Index. -* Data dimensionality reduction with Principal Component Analysis -* Automatic file management for storage of models to be reused during live -* Smart and safe data standardization -* Cleaning of NaNs from the data set before training and prediction. -* Automated live retraining (still VERY experimental. Proceed with caution.) +* Create large rich feature sets (10k+ features) based on simple user created strategies. +* Sweep model training and backtesting to simulate consistent model retraining through time. +* Remove outliers automatically from training and prediction sets using a Dissimilarity Index and Support Vector Machines. +* Reduce the dimensionality of the data with Principal Component Analysis. +* Store models to disk to make reloading from a crash fast and easy (and purge obsolete files automatically for sustained dry/live runs.) +* Normalize the data automatically in a smart and statistically safe way. +* Automated data download and data handling. +* Clean the incoming data and of NaNs in a safe way and before training and prediction. +* Retrain live automatically so that the model self-adapts to the market in an unsupervised manner. ## General approach The user provides FreqAI with a set of custom indicators (created inside the strategy the same way -a typical Freqtrade strategy is created) as well as a target value (typically some price change into -the future). FreqAI trains a model to predict the target value based on the input of custom indicators. +a typical Freqtrade strategy is created) as well as a target value (typically some price change into the future). FreqAI trains a model to predict the target value based on the input of custom indicators. FreqAI will train and save a new model for each pair in the config whitelist. -Users employ FreqAI to backtest a strategy (emulate reality with retraining a model as new data is -introduced) and run the model live to generate buy and sell signals. +Users employ FreqAI to backtest a strategy (emulate reality with retraining a model as new data is introduced) and run the model live to generate buy and sell signals. In dry/live, FreqAI works in a background thread to keep all models as updated as possible with consistent retraining. ## Background and vocabulary @@ -58,17 +54,55 @@ Use `pip` to install the prerequisites with: ## Running from the example files An example strategy, an example prediction model, and example config can all be found in -`freqtrade/templates/ExampleFreqaiStrategy.py`, -`freqtrade/freqai/prediction_models/CatboostPredictionModel.py`, -`config_examples/config_freqai.example.json`, respectively. Assuming the user has downloaded +`freqtrade/templates/FreqaiExampleStrategy.py`, +`freqtrade/freqai/prediction_models/LightGBMPredictionModel.py`, +`config_examples/config_freqai_futures.example.json`, respectively. Assuming the user has downloaded the necessary data, Freqai can be executed from these templates with: ```bash -freqtrade backtesting --config config_examples/config_freqai.example.json --strategy FreqaiExampleStrategy --freqaimodel CatboostPredictionModel --strategy-path freqtrade/templates --timerange 20220101-20220201 +freqtrade backtesting --config config_examples/config_freqai.example.json --strategy FreqaiExampleStrategy --freqaimodel LightGBMPredictionModel --strategy-path freqtrade/templates --timerange 20220101-20220201 ``` ## Configuring the bot +The table below will list all configuration parameters available for `FreqAI`. + +Mandatory parameters are marked as **Required**, which means that they are required to be set in one of the possible ways. + +| Parameter | Description | +|------------|-------------| +| `freqai` | **Required.** The dictionary containing all the parameters for controlling FreqAI.
**Datatype:** dictionary. +| `identifier` | **Required.** A unique name for the current model. This can be reused to reload pretrained models/data.
**Datatype:** string. +| `train_period_days` | **Required.** Number of days to use for the training data (width of the sliding window).
**Datatype:** positive integer. +| `backtest_period_days` | **Required.** Number of days to inference into the trained model before sliding the window and retraining. This can be fractional days, but beware that the user provided `timerange` will be divided by this number to yield the number of trainings necessary to complete the backtest.
**Datatype:** Float. +| `live_retrain_hours` | Frequency of retraining during dry/live runs. Default set to 0, which means it will retrain as often as possible. **Datatype:** Float > 0. +| `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. False by default.
**Datatype:** boolean. +| `live_trained_timestamp` | Useful if user wants to start from models trained during a *backtest*. The timestamp can be located in the `user_data/models` backtesting folder. This is not a commonly used parameter, leave undefined for most applications.
**Datatype:** positive integer. +| `fit_live_predictions_candles` | Computes target (label) statistics from prediction data, instead of from the training data set. Number of candles is the number of historical candles it uses to generate the statistics.
**Datatype:** positive integer. +| | **Feature Parameters** +| `feature_parameters` | A dictionary containing the parameters used to engineer the feature set. Details and examples shown [here](#building-the-feature-set)
**Datatype:** dictionary. +| `include_corr_pairlist` | A list of correlated coins that FreqAI will add as additional features to all `pair_whitelist` coins. All indicators set in `populate_any_indicators` will be created for each coin in this list, and that set of features is added to the base asset feature set.
**Datatype:** list of assets (strings). +| `include_timeframes` | A list of timeframes that all indicators in `populate_any_indicators` will be created for and added as features to the base asset feature set.
**Datatype:** list of timeframes (strings). +| `label_period_candles` | Number of candles into the future that the labels are created for. This is used in `populate_any_indicators`, refer to `templates/FreqaiExampleStrategy.py` for detailed usage. The user can create custom labels, making use of this parameter not.
**Datatype:** positive integer. +| `include_shifted_candles` | Parameter used to add a sense of temporal recency to flattened regression type input data. `include_shifted_candles` takes all features, duplicates and shifts them by the number indicated by user.
**Datatype:** positive integer. +| `DI_threshold` | Activates the Dissimilarity Index for outlier detection when above 0, explained more [here](#removing-outliers-with-the-dissimilarity-index).
**Datatype:** positive float (typically below 1). +| `weight_factor` | Used to set weights for training data points according to their recency, see details and a figure of how it works [here](##controlling-the-model-learning-process).
**Datatype:** positive float (typically below 1). +| `principal_component_analysis` | Ask FreqAI to automatically reduce the dimensionality of the data set using PCA.
**Datatype:** boolean. +| `use_SVM_to_remove_outliers` | Ask FreqAI to train a support vector machine to detect and remove outliers from the training data set as well as from incoming data points.
**Datatype:** boolean. +| `svm_nu` | The `nu` parameter for the support vector machine. *Very* broadly, this is the percentage of data points that should be considered outliers.
**Datatype:** float between 0 and 1. +| `stratify_training_data` | This value is used to indicate the stratification of the data. e.g. 2 would set every 2nd data point into a separate dataset to be pulled from during training/testing.
**Datatype:** positive integer. +| `indicator_max_period_candles` | The maximum *period* used in `populate_any_indicators()` for indicator creation. FreqAI uses this information in combination with the maximum timeframe to calculate how many data points it should download so that the first data point does not have a NaN
**Datatype:** positive integer. +| `indicator_periods_candles` | A list of integers used to duplicate all indicators according to a set of periods and add them to the feature set.
**Datatype:** list of positive integers. +| | **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)
**Datatype:** dictionary. +| `test_size` | Fraction of data that should be used for testing instead of training.
**Datatype:** positive float below 1. +| `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 library. For example, if the user uses `LightGBMPredictionModel`, then this dictionary can contain any parameter available by the `LightGBMRegressor` [here](https://lightgbm.readthedocs.io/en/latest/pythonapi/lightgbm.LGBMRegressor.html). If the user selects a different model, then this dictionary can contain any parameter from that different model.
**Datatype:** dictionary. +| `n_estimators` | A common parameter among regressors which sets the number of boosted trees to fit
**Datatype:** integer. +| `learning_rate` | A common parameter among regressors which sets the boosting learning rate.
**Datatype:** float. +| `n_jobs`, `thread_count`, `task_type` | Different libraries use different parameter names to control the number of threads used for parallel processing or whether or not it is a `task_type` of `gpu` or `cpu`.
**Datatype:** float. + ### Example config file The user interface is isolated to the typical config file. A typical Freqai @@ -115,7 +149,7 @@ components/structures that the user *must* include when building their feature s `with self.model.bridge.lock:` must be used to ensure thread safety - especially when using third party libraries for indicator construction such as TA-lib. Another structure to consider is the location of the labels at the bottom of the example function (below `if set_generalized_indicators:`). -This is where the user will add single features labels to their feature set to avoid duplication from +This is where the user will add single features and labels to their feature set to avoid duplication from various configuration paramters which multiply the feature set such as `include_timeframes`. ```python @@ -213,14 +247,14 @@ a specific pair or timeframe, they should use the following structure inside `po (as exemplified in `freqtrade/templates/FreqaiExampleStrategy.py`: ```python - def populate_any_indicators(self, metadata, pair, df, tf, informative=None, coin=""): + def populate_any_indicators(self, metadata, pair, df, tf, informative=None, coin="", set_generalized_indicators=False): ... # Add generalized indicators here (because in live, it will call only this function to populate # indicators for retraining). Notice how we ensure not to add them multiple times by associating # these generalized indicators to the basepair/timeframe - if pair == metadata['pair'] and tf == self.timeframe: + if set_generalized_indicators: df['%-day_of_week'] = (df["date"].dt.dayofweek + 1) / 7 df['%-hour_of_day'] = (df['date'].dt.hour + 1) / 25 @@ -292,7 +326,7 @@ and adding this to the `train_period_days`. The units need to be in the base can The freqai training/backtesting module can be executed with the following command: ```bash -freqtrade backtesting --strategy FreqaiExampleStrategy --config config_freqai.example.json --freqaimodel CatboostPredictionModel --timerange 20210501-20210701 +freqtrade backtesting --strategy FreqaiExampleStrategy --config config_freqai_futures.example.json --freqaimodel LightGBMPredictionModel --timerange 20210501-20210701 ``` If this command has never been executed with the existing config file, then it will train a new model @@ -334,32 +368,15 @@ The Freqai strategy requires the user to include the following lines of code in def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: self.freqai_info = self.config["freqai"] - self.pair = metadata["pair"] - sgi = True - # the following loops are necessary for building the features - # indicated by the user in the configuration file. + # All indicators must be populated by populate_any_indicators() for live functionality # to work correctly. - for tf in self.freqai_info["feature_parameters"]["include_timeframes"]: - dataframe = self.populate_any_indicators( - metadata, - self.pair, - dataframe.copy(), - tf, - coin=self.pair.split("/")[0] + "-", - set_generalized_indicators=sgi, - ) - sgi = False - for pair in self.freqai_info["feature_parameters"]["include_corr_pairlist"]: - if metadata["pair"] in pair: - continue # do not include whitelisted pair twice if it is in corr_pairlist - dataframe = self.populate_any_indicators( - metadata, pair, dataframe.copy(), tf, coin=pair.split("/")[0] + "-" - ) - # the model will return 4 values, its prediction, an indication of whether or not the - # prediction should be accepted, the target mean/std values from the labels used during - # each training period. + # the model will return all labels created by user in `populate_any_indicators` + # (& appended targets), an indication of whether or not the prediction should be accepted, + # the target mean/std values for each of the labels created by user in + # `populate_any_indicators()` for each training period. + dataframe = self.model.bridge.start(dataframe, metadata, self) return dataframe @@ -370,7 +387,7 @@ the feature set with a proper naming convention for the IFreqaiModel to use late ### Building an IFreqaiModel -Freqai has an example prediction model based on the popular `Catboost` regression (`freqai/prediction_models/CatboostPredictionModel.py`). However, users can customize and create +FreqAI has multiple example prediction model based libraries such as `Catboost` regression (`freqai/prediction_models/CatboostPredictionModel.py`) and `LightGBM` regression. However, users can customize and create their own prediction models using the `IFreqaiModel` class. Users are encouraged to inherit `train()` and `predict()` to let them customize various aspects of their training procedures. ### Running the model live @@ -443,7 +460,7 @@ $\overline{d}$ quantifies the spread of the training data, which is compared to the distance between the new prediction feature vectors, $X_k$ and all the training data: -$$ d_k = \argmin_i d_{k,i} $$ +$$ d_k = \arg \min d_{k,i} $$ which enables the estimation of a Dissimilarity Index: @@ -635,6 +652,14 @@ below this value. An example usage in the strategy may look something like: ## Additional information +### Common pitfalls + +FreqAI cannot be combined with `VolumePairlists` (or any pairlist filter that adds and removes pairs dynamically). +This is for performance reasons - FreqAI relies on making quick predictions/retrains. To do this effectively, +it needs to download all the training data at the beginning of a dry/live instance. FreqAI stores and appends +new candles automatically for future retrains. But this means that if new pairs arrive later in the dry run due +to a volume pairlist, it will not have the data ready. FreqAI does work, however, with the `ShufflePairlist`. + ### Feature normalization The feature set created by the user is automatically normalized to the training diff --git a/freqtrade/freqai/data_drawer.py b/freqtrade/freqai/data_drawer.py index 5488a7e6b..c89394c09 100644 --- a/freqtrade/freqai/data_drawer.py +++ b/freqtrade/freqai/data_drawer.py @@ -301,7 +301,7 @@ class FreqaiDataDrawer: model_folders = [x for x in self.full_path.iterdir() if x.is_dir()] - pattern = re.compile(r"sub-train-(\w+)(\d{10})") + pattern = re.compile(r"sub-train-(\w+)_(\d{10})") delete_dict: Dict[str, Any] = {} diff --git a/freqtrade/freqai/data_kitchen.py b/freqtrade/freqai/data_kitchen.py index 8050b8b3b..132faaa86 100644 --- a/freqtrade/freqai/data_kitchen.py +++ b/freqtrade/freqai/data_kitchen.py @@ -88,7 +88,8 @@ class FreqaiDataKitchen: ) self.data_path = Path( - self.full_path / str("sub-train" + "-" + pair.split("/")[0] + str(trained_timestamp)) + self.full_path + / str("sub-train" + "-" + pair.split("/")[0] + "_" + str(trained_timestamp)) ) return @@ -179,6 +180,7 @@ class FreqaiDataKitchen: model = load(self.data_path / str(self.model_filename + "_model.joblib")) else: from tensorflow import keras + model = keras.models.load_model(self.data_path / str(self.model_filename + "_model.h5")) if Path(self.data_path / str(self.model_filename + "_svm_model.joblib")).resolve().exists(): @@ -410,6 +412,10 @@ class FreqaiDataKitchen: bt_split: the backtesting length (dats). Specified in user configuration file """ + if not isinstance(train_split, int) or train_split < 1: + raise OperationalException( + "train_period_days must be an integer greater than 0. " f"Got {train_split}." + ) train_period_days = train_split * SECONDS_IN_DAY bt_period = bt_split * SECONDS_IN_DAY @@ -561,8 +567,10 @@ class FreqaiDataKitchen: """ if self.keras: - logger.warning("SVM outlier removal not currently supported for Keras based models. " - "Skipping user requested function.") + logger.warning( + "SVM outlier removal not currently supported for Keras based models. " + "Skipping user requested function." + ) if predict: self.do_predict = np.ones(len(self.data_dictionary["prediction_features"])) return @@ -676,8 +684,7 @@ class FreqaiDataKitchen: training than older data. """ wfactor = self.config["freqai"]["feature_parameters"]["weight_factor"] - weights = np.exp( - - np.arange(num_weights) / (wfactor * num_weights))[::-1] + weights = np.exp(-np.arange(num_weights) / (wfactor * num_weights))[::-1] return weights def append_predictions(self, predictions, do_predict, len_dataframe): @@ -685,8 +692,6 @@ class FreqaiDataKitchen: Append backtest prediction from current backtest period to all previous periods """ - # ones = np.ones(len(predictions)) - # target_mean, target_std = ones * self.data["target_mean"], ones * self.data["target_std"] self.append_df = DataFrame() for label in self.label_list: self.append_df[label] = predictions[label] @@ -702,13 +707,6 @@ class FreqaiDataKitchen: else: self.full_df = pd.concat([self.full_df, self.append_df], axis=0) - # self.full_predictions = np.append(self.full_predictions, predictions) - # self.full_do_predict = np.append(self.full_do_predict, do_predict) - # if self.freqai_config.get("feature_parameters", {}).get("DI_threshold", 0) > 0: - # self.full_DI_values = np.append(self.full_DI_values, self.DI_values) - # self.full_target_mean = np.append(self.full_target_mean, target_mean) - # self.full_target_std = np.append(self.full_target_std, target_std) - return def fill_predictions(self, dataframe): @@ -729,25 +727,34 @@ class FreqaiDataKitchen: self.append_df = DataFrame() self.full_df = DataFrame() - # self.full_predictions = np.append(filler, self.full_predictions) - # self.full_do_predict = np.append(filler, self.full_do_predict) - # if self.freqai_config.get("feature_parameters", {}).get("DI_threshold", 0) > 0: - # self.full_DI_values = np.append(filler, self.full_DI_values) - # self.full_target_mean = np.append(filler, self.full_target_mean) - # self.full_target_std = np.append(filler, self.full_target_std) return def create_fulltimerange(self, backtest_tr: str, backtest_period_days: int) -> str: + + if not isinstance(backtest_period_days, int): + raise OperationalException("backtest_period_days must be an integer") + + if backtest_period_days < 0: + raise OperationalException("backtest_period_days must be positive") + backtest_timerange = TimeRange.parse_timerange(backtest_tr) if backtest_timerange.stopts == 0: - backtest_timerange.stopts = int( - datetime.datetime.now(tz=datetime.timezone.utc).timestamp() - ) + # typically open ended time ranges do work, however, there are some edge cases where + # it does not. accomodating these kinds of edge cases just to allow open-ended + # timerange is not high enough priority to warrant the effort. It is safer for now + # to simply ask user to add their end date + raise OperationalException("FreqAI backtesting does not allow open ended timeranges. " + "Please indicate the end date of your desired backtesting. " + "timerange.") + # backtest_timerange.stopts = int( + # datetime.datetime.now(tz=datetime.timezone.utc).timestamp() + # ) - backtest_timerange.startts = (backtest_timerange.startts - - backtest_period_days * SECONDS_IN_DAY) + 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) full_timerange = start.strftime("%Y%m%d") + "-" + stop.strftime("%Y%m%d") @@ -793,8 +800,9 @@ class FreqaiDataKitchen: data_load_timerange = TimeRange() # find the max indicator length required - max_timeframe_chars = self.freqai_config.get( - "feature_parameters", {}).get("include_timeframes")[-1] + max_timeframe_chars = self.freqai_config.get("feature_parameters", {}).get( + "include_timeframes" + )[-1] max_period = self.freqai_config.get("feature_parameters", {}).get( "indicator_max_period_candles", 50 ) @@ -861,35 +869,11 @@ class FreqaiDataKitchen: coin, _ = pair.split("/") self.data_path = Path( self.full_path - / str("sub-train" + "-" + pair.split("/")[0] + str(int(trained_timerange.stopts))) + / str("sub-train" + "-" + pair.split("/")[0] + "_" + str(int(trained_timerange.stopts))) ) self.model_filename = "cb_" + coin.lower() + "_" + str(int(trained_timerange.stopts)) - # self.freqai_config['live_trained_timerange'] = str(int(trained_timerange.stopts)) - # enables persistence, but not fully implemented into save/load data yer - # self.data['live_trained_timerange'] = str(int(trained_timerange.stopts)) - - # SUPERCEDED - # def download_new_data_for_retraining(self, timerange: TimeRange, metadata: dict, - # strategy: IStrategy) -> None: - - # exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], - # self.config, validate=False, freqai=True) - # # exchange = strategy.dp._exchange # closes ccxt session - # pairs = copy.deepcopy(self.freqai_config.get('corr_pairlist', [])) - # if str(metadata['pair']) not in pairs: - # pairs.append(str(metadata['pair'])) - - # refresh_backtest_ohlcv_data( - # exchange, pairs=pairs, timeframes=self.freqai_config.get('timeframes'), - # datadir=self.config['datadir'], timerange=timerange, - # new_pairs_days=self.config['new_pairs_days'], - # erase=False, data_format=self.config.get('dataformat_ohlcv', 'json'), - # trading_mode=self.config.get('trading_mode', 'spot'), - # prepend=self.config.get('prepend_data', False) - # ) - def download_all_data_for_training(self, timerange: TimeRange) -> None: """ Called only once upon start of bot to download the necessary data for @@ -969,8 +953,9 @@ class FreqaiDataKitchen: def set_all_pairs(self) -> None: - self.all_pairs = copy.deepcopy(self.freqai_config.get( - 'feature_parameters', {}).get('include_corr_pairlist', [])) + self.all_pairs = copy.deepcopy( + self.freqai_config.get("feature_parameters", {}).get("include_corr_pairlist", []) + ) for pair in self.config.get("exchange", "").get("pair_whitelist"): if pair not in self.all_pairs: self.all_pairs.append(pair) @@ -1014,8 +999,9 @@ class FreqaiDataKitchen: corr_dataframes: Dict[Any, Any] = {} base_dataframes: Dict[Any, Any] = {} historic_data = self.dd.historic_data - pairs = self.freqai_config.get('feature_parameters', {}).get( - 'include_corr_pairlist', []) + pairs = self.freqai_config.get("feature_parameters", {}).get( + "include_corr_pairlist", [] + ) for tf in self.freqai_config.get("feature_parameters", {}).get("include_timeframes"): base_dataframes[tf] = self.slice_dataframe(timerange, historic_data[pair][tf]) @@ -1031,40 +1017,13 @@ class FreqaiDataKitchen: return corr_dataframes, base_dataframes - # SUPERCEDED - # def load_pairs_histories(self, timerange: TimeRange, metadata: dict) -> Tuple[Dict[Any, Any], - # DataFrame]: - # corr_dataframes: Dict[Any, Any] = {} - # base_dataframes: Dict[Any, Any] = {} - # pairs = self.freqai_config.get('include_corr_pairlist', []) # + [metadata['pair']] - # # timerange = TimeRange.parse_timerange(new_timerange) - - # for tf in self.freqai_config.get('timeframes'): - # base_dataframes[tf] = load_pair_history(datadir=self.config['datadir'], - # timeframe=tf, - # pair=metadata['pair'], timerange=timerange, - # data_format=self.config.get( - # 'dataformat_ohlcv', 'json'), - # candle_type=self.config.get( - # 'trading_mode', 'spot')) - # if pairs: - # for p in pairs: - # if metadata['pair'] in p: - # continue # dont repeat anything from whitelist - # if p not in corr_dataframes: - # corr_dataframes[p] = {} - # corr_dataframes[p][tf] = load_pair_history(datadir=self.config['datadir'], - # timeframe=tf, - # pair=p, timerange=timerange, - # data_format=self.config.get( - # 'dataformat_ohlcv', 'json'), - # candle_type=self.config.get( - # 'trading_mode', 'spot')) - - # return corr_dataframes, base_dataframes - def use_strategy_to_populate_indicators( - self, strategy: IStrategy, corr_dataframes: dict, base_dataframes: dict, pair: str + self, + strategy: IStrategy, + corr_dataframes: dict = {}, + base_dataframes: dict = {}, + pair: str = "", + prediction_dataframe: DataFrame = pd.DataFrame(), ) -> DataFrame: """ Use the user defined strategy for populating indicators during @@ -1079,16 +1038,31 @@ class FreqaiDataKitchen: :returns: dataframe: DataFrame = dataframe containing populated indicators """ - dataframe = base_dataframes[self.config["timeframe"]].copy() - pairs = self.freqai_config.get('feature_parameters', {}).get('include_corr_pairlist', []) + + # for prediction dataframe creation, we let dataprovider handle everything in the strategy + # so we create empty dictionaries, which allows us to pass None to + # `populate_any_indicators()`. Signaling we want the dp to give us the live dataframe. + tfs = self.freqai_config.get("feature_parameters", {}).get("include_timeframes") + pairs = self.freqai_config.get("feature_parameters", {}).get("include_corr_pairlist", []) + if not prediction_dataframe.empty: + dataframe = prediction_dataframe.copy() + for tf in tfs: + base_dataframes[tf] = None + for p in pairs: + if p not in corr_dataframes: + corr_dataframes[p] = {} + corr_dataframes[p][tf] = None + else: + dataframe = base_dataframes[self.config["timeframe"]].copy() + sgi = True - for tf in self.freqai_config.get("feature_parameters", {}).get("include_timeframes"): + for tf in tfs: dataframe = strategy.populate_any_indicators( pair, pair, dataframe.copy(), tf, - base_dataframes[tf], + informative=base_dataframes[tf], coin=pair.split("/")[0] + "-", set_generalized_indicators=sgi, ) @@ -1102,7 +1076,7 @@ class FreqaiDataKitchen: i, dataframe.copy(), tf, - corr_dataframes[i][tf], + informative=corr_dataframes[i][tf], coin=i.split("/")[0] + "-", ) @@ -1113,7 +1087,8 @@ class FreqaiDataKitchen: Fit the labels with a gaussian distribution """ import scipy as spy - num_candles = self.freqai_config.get('fit_live_predictions_candles', 100) + + num_candles = self.freqai_config.get("fit_live_predictions_candles", 100) self.data["labels_mean"], self.data["labels_std"] = {}, {} for label in self.label_list: f = spy.stats.norm.fit(self.dd.historic_predictions[self.pair][label].tail(num_candles)) diff --git a/freqtrade/freqai/freqai_interface.py b/freqtrade/freqai/freqai_interface.py index 56a179dc3..1d152b702 100644 --- a/freqtrade/freqai/freqai_interface.py +++ b/freqtrade/freqai/freqai_interface.py @@ -73,6 +73,8 @@ class IFreqaiModel(ABC): self.freqai_info["feature_parameters"]["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) + self.pair_it = 0 + self.total_pairs = len(self.config.get("exchange", {}).get("pair_whitelist")) def assert_config(self, config: Dict[str, Any]) -> None: @@ -106,6 +108,10 @@ class IFreqaiModel(ABC): elif not self.follow_mode: self.dk = FreqaiDataKitchen(self.config, self.dd, self.live, metadata["pair"]) logger.info(f"Training {len(self.dk.training_timeranges)} timeranges") + + dataframe = self.dk.use_strategy_to_populate_indicators( + strategy, prediction_dataframe=dataframe, pair=metadata["pair"] + ) dk = self.start_backtesting(dataframe, metadata, self.dk) dataframe = self.remove_features_from_df(dk.return_dataframe) @@ -160,6 +166,8 @@ class IFreqaiModel(ABC): dk: FreqaiDataKitchen = Data management/analysis tool assoicated to present pair only """ + self.pair_it += 1 + train_it = 0 # Loop enforcing the sliding window training/backtesting paradigm # tr_train is the training time range e.g. 1 historical month # tr_backtest is the backtesting time range e.g. the week directly @@ -167,22 +175,26 @@ class IFreqaiModel(ABC): # entire backtest for tr_train, tr_backtest in zip(dk.training_timeranges, dk.backtesting_timeranges): (_, _, _, _) = self.dd.get_pair_dict_info(metadata["pair"]) + train_it += 1 + total_trains = len(dk.backtesting_timeranges) gc.collect() dk.data = {} # clean the pair specific data between training window sliding self.training_timerange = tr_train - # self.training_timerange_timerange = tr_train dataframe_train = dk.slice_dataframe(tr_train, dataframe) dataframe_backtest = dk.slice_dataframe(tr_backtest, dataframe) - trained_timestamp = tr_train # TimeRange.parse_timerange(tr_train) + trained_timestamp = tr_train tr_train_startts_str = datetime.datetime.utcfromtimestamp(tr_train.startts).strftime( "%Y-%m-%d %H:%M:%S" ) tr_train_stopts_str = datetime.datetime.utcfromtimestamp(tr_train.stopts).strftime( "%Y-%m-%d %H:%M:%S" ) - logger.info("Training %s", metadata["pair"]) - logger.info(f"Training {tr_train_startts_str} to {tr_train_stopts_str}") + 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} " + "trains" + ) dk.data_path = Path( dk.full_path @@ -190,6 +202,7 @@ class IFreqaiModel(ABC): "sub-train" + "-" + metadata["pair"].split("/")[0] + + "_" + str(int(trained_timestamp.stopts)) ) ) @@ -281,6 +294,10 @@ class IFreqaiModel(ABC): # load the model and associated data into the data kitchen self.model = dk.load_data(coin=metadata["pair"]) + dataframe = self.dk.use_strategy_to_populate_indicators( + strategy, prediction_dataframe=dataframe, pair=metadata["pair"] + ) + if not self.model: logger.warning( f"No model ready for {metadata['pair']}, returning null values to strategy." diff --git a/freqtrade/templates/FreqaiExampleStrategy.py b/freqtrade/templates/FreqaiExampleStrategy.py index ca08b8168..402aa9d1c 100644 --- a/freqtrade/templates/FreqaiExampleStrategy.py +++ b/freqtrade/templates/FreqaiExampleStrategy.py @@ -171,32 +171,15 @@ class FreqaiExampleStrategy(IStrategy): def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: self.freqai_info = self.config["freqai"] - self.pair = metadata["pair"] - sgi = True - # the following loops are necessary for building the features - # indicated by the user in the configuration file. + # All indicators must be populated by populate_any_indicators() for live functionality # to work correctly. - for tf in self.freqai_info["feature_parameters"]["include_timeframes"]: - dataframe = self.populate_any_indicators( - metadata, - self.pair, - dataframe.copy(), - tf, - coin=self.pair.split("/")[0] + "-", - set_generalized_indicators=sgi, - ) - sgi = False - for pair in self.freqai_info["feature_parameters"]["include_corr_pairlist"]: - if metadata["pair"] in pair: - continue # do not include whitelisted pair twice if it is in corr_pairlist - dataframe = self.populate_any_indicators( - metadata, pair, dataframe.copy(), tf, coin=pair.split("/")[0] + "-" - ) - # the model will return 4 values, its prediction, an indication of whether or not the - # prediction should be accepted, the target mean/std values from the labels used during - # each training period. + # the model will return all labels created by user in `populate_any_indicators` + # (& appended targets), an indication of whether or not the prediction should be accepted, + # the target mean/std values for each of the labels created by user in + # `populate_any_indicators()` for each training period. + dataframe = self.model.bridge.start(dataframe, metadata, self) dataframe["target_roi"] = dataframe["&-s_close_mean"] + dataframe["&-s_close_std"] * 1.25 diff --git a/mkdocs.yml b/mkdocs.yml index 18744e0d5..b084b59a7 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -35,7 +35,7 @@ nav: - Edge Positioning: edge.md - Advanced Strategy: strategy-advanced.md - Advanced Hyperopt: advanced-hyperopt.md - - Freqai: freqai.md + - FreqAI: freqai.md - Sandbox Testing: sandbox-testing.md - FAQ: faq.md - SQL Cheat-sheet: sql_cheatsheet.md diff --git a/requirements-dev.txt b/requirements-dev.txt index f2f77c2ba..16da829c4 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -2,6 +2,7 @@ -r requirements.txt -r requirements-plot.txt -r requirements-hyperopt.txt +-r requirements-freqai.txt -r docs/requirements-docs.txt coveralls==3.3.1 diff --git a/requirements-freqai.txt b/requirements-freqai.txt index a06a41b96..c3aa2e4db 100644 --- a/requirements-freqai.txt +++ b/requirements-freqai.txt @@ -2,7 +2,7 @@ -r requirements.txt # Required for freqai -scikit-learn==1.0.2 +scikit-learn==1.1.1 scikit-optimize==0.9.0 joblib==1.1.0 catboost==1.0.4 diff --git a/setup.sh b/setup.sh index 202cb70c7..1a4a285a3 100755 --- a/setup.sh +++ b/setup.sh @@ -77,7 +77,15 @@ function updateenv() { fi fi - ${PYTHON} -m pip install --upgrade -r ${REQUIREMENTS} ${REQUIREMENTS_HYPEROPT} ${REQUIREMENTS_PLOT} + REQUIREMENTS_FREQAI="" + read -p "Do you want to install dependencies for freqai [y/N]? " + dev=$REPLY + if [[ $REPLY =~ ^[Yy]$ ]] + then + REQUIREMENTS_FREQAI="-r requirements-freqai.txt" + fi + + ${PYTHON} -m pip install --upgrade -r ${REQUIREMENTS} ${REQUIREMENTS_HYPEROPT} ${REQUIREMENTS_PLOT} ${REQUIREMENTS_FREQAI} if [ $? -ne 0 ]; then echo "Failed installing dependencies" exit 1 diff --git a/tests/freqai/conftest.py b/tests/freqai/conftest.py new file mode 100644 index 000000000..549ba2663 --- /dev/null +++ b/tests/freqai/conftest.py @@ -0,0 +1,117 @@ +from copy import deepcopy +from pathlib import Path +from unittest.mock import MagicMock + +from freqtrade.configuration import TimeRange +from freqtrade.data.dataprovider import DataProvider +from freqtrade.freqai.data_kitchen import FreqaiDataKitchen +from freqtrade.resolvers import StrategyResolver +from freqtrade.resolvers.freqaimodel_resolver import FreqaiModelResolver +from tests.conftest import get_patched_exchange + + +# @pytest.fixture(scope="function") +def freqai_conf(default_conf): + freqaiconf = deepcopy(default_conf) + freqaiconf.update( + { + "datadir": Path(default_conf["datadir"]), + "strategy": "freqai_test_strat", + "strategy-path": "freqtrade/tests/strategy/strats", + "freqaimodel": "LightGBMPredictionModel", + "freqaimodel_path": "freqai/prediction_models", + "timerange": "20180110-20180115", + "freqai": { + "startup_candles": 10000, + "purge_old_models": True, + "train_period_days": 5, + "backtest_period_days": 2, + "live_retrain_hours": 0, + "expiration_hours": 1, + "identifier": "uniqe-id100", + "live_trained_timestamp": 0, + "feature_parameters": { + "include_timeframes": ["5m"], + "include_corr_pairlist": ["ADA/BTC", "DASH/BTC"], + "label_period_candles": 20, + "include_shifted_candles": 1, + "DI_threshold": 0.9, + "weight_factor": 0.9, + "principal_component_analysis": False, + "use_SVM_to_remove_outliers": True, + "stratify_training_data": 0, + "indicator_max_period_candles": 10, + "indicator_periods_candles": [10], + }, + "data_split_parameters": {"test_size": 0.33, "random_state": 1}, + "model_training_parameters": {"n_estimators": 100, "verbosity": 0}, + }, + "config_files": [Path('config_examples', 'config_freqai_futures.example.json')] + } + ) + freqaiconf['exchange'].update({'pair_whitelist': ['ADA/BTC', 'DASH/BTC', 'ETH/BTC', 'LTC/BTC']}) + return freqaiconf + + +def get_patched_data_kitchen(mocker, freqaiconf): + dd = mocker.patch('freqtrade.freqai.data_drawer', MagicMock()) + dk = FreqaiDataKitchen(freqaiconf, dd) + return dk + + +def get_patched_freqai_strategy(mocker, freqaiconf): + strategy = StrategyResolver.load_strategy(freqaiconf) + strategy.bot_start() + + return strategy + + +def get_patched_freqaimodel(mocker, freqaiconf): + freqaimodel = FreqaiModelResolver.load_freqaimodel(freqaiconf) + + return freqaimodel + + +def get_freqai_live_analyzed_dataframe(mocker, freqaiconf): + strategy = get_patched_freqai_strategy(mocker, freqaiconf) + exchange = get_patched_exchange(mocker, freqaiconf) + strategy.dp = DataProvider(freqaiconf, exchange) + freqai = strategy.model.bridge + freqai.live = True + freqai.dk = FreqaiDataKitchen(freqaiconf, freqai.dd) + timerange = TimeRange.parse_timerange("20180110-20180114") + freqai.dk.load_all_pair_histories(timerange) + + strategy.analyze_pair('ADA/BTC', '5m') + return strategy.dp.get_analyzed_dataframe('ADA/BTC', '5m') + + +def get_freqai_analyzed_dataframe(mocker, freqaiconf): + strategy = get_patched_freqai_strategy(mocker, freqaiconf) + exchange = get_patched_exchange(mocker, freqaiconf) + strategy.dp = DataProvider(freqaiconf, exchange) + strategy.freqai_info = freqaiconf.get("freqai", {}) + freqai = strategy.model.bridge + freqai.live = True + freqai.dk = FreqaiDataKitchen(freqaiconf, freqai.dd) + timerange = TimeRange.parse_timerange("20180110-20180114") + freqai.dk.load_all_pair_histories(timerange) + sub_timerange = TimeRange.parse_timerange("20180111-20180114") + corr_df, base_df = freqai.dk.get_base_and_corr_dataframes(sub_timerange, "LTC/BTC") + + return freqai.dk.use_strategy_to_populate_indicators(strategy, corr_df, base_df, 'LTC/BTC') + + +def get_ready_to_train(mocker, freqaiconf): + strategy = get_patched_freqai_strategy(mocker, freqaiconf) + exchange = get_patched_exchange(mocker, freqaiconf) + strategy.dp = DataProvider(freqaiconf, exchange) + strategy.freqai_info = freqaiconf.get("freqai", {}) + freqai = strategy.model.bridge + freqai.live = True + freqai.dk = FreqaiDataKitchen(freqaiconf, freqai.dd) + timerange = TimeRange.parse_timerange("20180110-20180114") + freqai.dk.load_all_pair_histories(timerange) + sub_timerange = TimeRange.parse_timerange("20180111-20180114") + corr_df, base_df = freqai.dk.get_base_and_corr_dataframes(sub_timerange, "LTC/BTC") + return corr_df, base_df, freqai, strategy diff --git a/tests/freqai/test_freqai_datakitchen.py b/tests/freqai/test_freqai_datakitchen.py new file mode 100644 index 000000000..1964d1423 --- /dev/null +++ b/tests/freqai/test_freqai_datakitchen.py @@ -0,0 +1,167 @@ +# from unittest.mock import MagicMock +# from freqtrade.commands.optimize_commands import setup_optimize_configuration, start_edge +import copy +import datetime +import shutil +from pathlib import Path + +import pytest + +from freqtrade.configuration import TimeRange +from freqtrade.data.dataprovider import DataProvider +# from freqtrade.freqai.data_drawer import FreqaiDataDrawer +from freqtrade.exceptions import OperationalException +from freqtrade.freqai.data_kitchen import FreqaiDataKitchen +from tests.conftest import get_patched_exchange +from tests.freqai.conftest import freqai_conf, get_patched_data_kitchen, get_patched_freqai_strategy + + +@pytest.mark.parametrize( + "timerange, train_period_days, expected_result", + [ + ("20220101-20220201", 30, "20211202-20220201"), + ("20220301-20220401", 15, "20220214-20220401"), + ], +) +def test_create_fulltimerange( + timerange, train_period_days, expected_result, default_conf, mocker, caplog +): + dk = get_patched_data_kitchen(mocker, freqai_conf(copy.deepcopy(default_conf))) + assert dk.create_fulltimerange(timerange, train_period_days) == expected_result + shutil.rmtree(Path(dk.full_path)) + + +def test_create_fulltimerange_incorrect_backtest_period(mocker, default_conf): + dk = get_patched_data_kitchen(mocker, freqai_conf(copy.deepcopy(default_conf))) + with pytest.raises(OperationalException, match=r"backtest_period_days must be an integer"): + dk.create_fulltimerange("20220101-20220201", 0.5) + with pytest.raises(OperationalException, match=r"backtest_period_days must be positive"): + dk.create_fulltimerange("20220101-20220201", -1) + shutil.rmtree(Path(dk.full_path)) + + +@pytest.mark.parametrize( + "timerange, train_period_days, backtest_period_days, expected_result", + [ + ("20220101-20220201", 30, 7, 9), + ("20220101-20220201", 30, 0.5, 120), + ("20220101-20220201", 10, 1, 80), + ], +) +def test_split_timerange( + mocker, default_conf, timerange, train_period_days, backtest_period_days, expected_result +): + freqaiconf = freqai_conf(copy.deepcopy(default_conf)) + freqaiconf.update({"timerange": "20220101-20220401"}) + dk = get_patched_data_kitchen(mocker, freqaiconf) + tr_list, bt_list = dk.split_timerange(timerange, train_period_days, backtest_period_days) + assert len(tr_list) == len(bt_list) == expected_result + + with pytest.raises( + OperationalException, match=r"train_period_days must be an integer greater than 0." + ): + dk.split_timerange("20220101-20220201", -1, 0.5) + shutil.rmtree(Path(dk.full_path)) + + +def test_update_historic_data(mocker, default_conf): + freqaiconf = freqai_conf(copy.deepcopy(default_conf)) + strategy = get_patched_freqai_strategy(mocker, freqaiconf) + exchange = get_patched_exchange(mocker, freqaiconf) + strategy.dp = DataProvider(freqaiconf, exchange) + freqai = strategy.model.bridge + freqai.live = True + freqai.dk = FreqaiDataKitchen(freqaiconf, freqai.dd) + timerange = TimeRange.parse_timerange("20180110-20180114") + + freqai.dk.load_all_pair_histories(timerange) + historic_candles = len(freqai.dd.historic_data["ADA/BTC"]["5m"]) + dp_candles = len(strategy.dp.get_pair_dataframe("ADA/BTC", "5m")) + candle_difference = dp_candles - historic_candles + freqai.dk.update_historic_data(strategy) + + updated_historic_candles = len(freqai.dd.historic_data["ADA/BTC"]["5m"]) + + assert updated_historic_candles - historic_candles == candle_difference + shutil.rmtree(Path(freqai.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, default_conf, timestamp, expected): + freqaiconf = freqai_conf(copy.deepcopy(default_conf)) + dk = get_patched_data_kitchen(mocker, freqaiconf) + assert dk.check_if_model_expired(timestamp) == expected + shutil.rmtree(Path(dk.full_path)) + + +def test_load_all_pairs_histories(mocker, default_conf): + freqaiconf = freqai_conf(copy.deepcopy(default_conf)) + strategy = get_patched_freqai_strategy(mocker, freqaiconf) + exchange = get_patched_exchange(mocker, freqaiconf) + strategy.dp = DataProvider(freqaiconf, exchange) + freqai = strategy.model.bridge + freqai.live = True + freqai.dk = FreqaiDataKitchen(freqaiconf, freqai.dd) + timerange = TimeRange.parse_timerange("20180110-20180114") + freqai.dk.load_all_pair_histories(timerange) + + assert len(freqai.dd.historic_data.keys()) == len( + freqaiconf.get("exchange", {}).get("pair_whitelist") + ) + assert len(freqai.dd.historic_data["ADA/BTC"]) == len( + freqaiconf.get("freqai", {}).get("feature_parameters", {}).get("include_timeframes") + ) + shutil.rmtree(Path(freqai.dk.full_path)) + + +def test_get_base_and_corr_dataframes(mocker, default_conf): + freqaiconf = freqai_conf(copy.deepcopy(default_conf)) + strategy = get_patched_freqai_strategy(mocker, freqaiconf) + exchange = get_patched_exchange(mocker, freqaiconf) + strategy.dp = DataProvider(freqaiconf, exchange) + freqai = strategy.model.bridge + freqai.live = True + freqai.dk = FreqaiDataKitchen(freqaiconf, freqai.dd) + timerange = TimeRange.parse_timerange("20180110-20180114") + freqai.dk.load_all_pair_histories(timerange) + sub_timerange = TimeRange.parse_timerange("20180111-20180114") + corr_df, base_df = freqai.dk.get_base_and_corr_dataframes(sub_timerange, "LTC/BTC") + + num_tfs = len( + freqaiconf.get("freqai", {}).get("feature_parameters", {}).get("include_timeframes") + ) + + assert len(base_df.keys()) == num_tfs + + assert len(corr_df.keys()) == len( + freqaiconf.get("freqai", {}).get("feature_parameters", {}).get("include_corr_pairlist") + ) + + assert len(corr_df["ADA/BTC"].keys()) == num_tfs + shutil.rmtree(Path(freqai.dk.full_path)) + + +def test_use_strategy_to_populate_indicators(mocker, default_conf): + freqaiconf = freqai_conf(copy.deepcopy(default_conf)) + strategy = get_patched_freqai_strategy(mocker, freqaiconf) + exchange = get_patched_exchange(mocker, freqaiconf) + strategy.dp = DataProvider(freqaiconf, exchange) + strategy.freqai_info = freqaiconf.get("freqai", {}) + freqai = strategy.model.bridge + freqai.live = True + freqai.dk = FreqaiDataKitchen(freqaiconf, freqai.dd) + timerange = TimeRange.parse_timerange("20180110-20180114") + freqai.dk.load_all_pair_histories(timerange) + sub_timerange = TimeRange.parse_timerange("20180111-20180114") + corr_df, base_df = freqai.dk.get_base_and_corr_dataframes(sub_timerange, "LTC/BTC") + + df = freqai.dk.use_strategy_to_populate_indicators(strategy, corr_df, base_df, 'LTC/BTC') + + assert len(df.columns) == 45 + shutil.rmtree(Path(freqai.dk.full_path)) diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py new file mode 100644 index 000000000..9219baee3 --- /dev/null +++ b/tests/freqai/test_freqai_interface.py @@ -0,0 +1,181 @@ +# from unittest.mock import MagicMock +# from freqtrade.commands.optimize_commands import setup_optimize_configuration, start_edge +import copy +# import platform +import shutil +from pathlib import Path +from unittest.mock import MagicMock + +from freqtrade.configuration import TimeRange +from freqtrade.data.dataprovider import DataProvider +from freqtrade.freqai.data_kitchen import FreqaiDataKitchen +from tests.conftest import get_patched_exchange, log_has_re +from tests.freqai.conftest import freqai_conf, get_patched_freqai_strategy + + +def test_train_model_in_series_LightGBM(mocker, default_conf): + freqaiconf = freqai_conf(copy.deepcopy(default_conf)) + freqaiconf.update({"timerange": "20180110-20180130"}) + + strategy = get_patched_freqai_strategy(mocker, freqaiconf) + exchange = get_patched_exchange(mocker, freqaiconf) + strategy.dp = DataProvider(freqaiconf, exchange) + strategy.freqai_info = freqaiconf.get("freqai", {}) + freqai = strategy.model.bridge + freqai.live = True + freqai.dk = FreqaiDataKitchen(freqaiconf, freqai.dd) + timerange = TimeRange.parse_timerange("20180110-20180130") + freqai.dk.load_all_pair_histories(timerange) + + 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 / str(freqai.dk.model_filename + "_model.joblib")) + .resolve() + .exists() + ) + assert ( + Path(freqai.dk.data_path / str(freqai.dk.model_filename + "_metadata.json")) + .resolve() + .exists() + ) + assert ( + Path(freqai.dk.data_path / str(freqai.dk.model_filename + "_trained_df.pkl")) + .resolve() + .exists() + ) + assert ( + Path(freqai.dk.data_path / str(freqai.dk.model_filename + "_svm_model.joblib")) + .resolve() + .exists() + ) + + shutil.rmtree(Path(freqai.dk.full_path)) + + +# FIXME: hits segfault +# @pytest.mark.skipif("arm" in platform.uname()[-1], reason="no ARM..") +# def test_train_model_in_series_Catboost(mocker, default_conf): +# freqaiconf = freqai_conf(copy.deepcopy(default_conf)) +# freqaiconf.update({"timerange": "20180110-20180130"}) +# freqaiconf.update({"freqaimodel": "CatboostPredictionModel"}) +# strategy = get_patched_freqai_strategy(mocker, freqaiconf) +# exchange = get_patched_exchange(mocker, freqaiconf) +# strategy.dp = DataProvider(freqaiconf, exchange) +# strategy.freqai_info = freqaiconf.get("freqai", {}) +# freqai = strategy.model.bridge +# freqai.live = True +# freqai.dk = FreqaiDataKitchen(freqaiconf, freqai.dd) +# timerange = TimeRange.parse_timerange("20180110-20180130") +# freqai.dk.load_all_pair_histories(timerange) + +# 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 / str(freqai.dk.model_filename + "_model.joblib")) +# .resolve() +# .exists() +# ) +# assert ( +# Path(freqai.dk.data_path / str(freqai.dk.model_filename + "_metadata.json")) +# .resolve() +# .exists() +# ) +# assert ( +# Path(freqai.dk.data_path / str(freqai.dk.model_filename + "_trained_df.pkl")) +# .resolve() +# .exists() +# ) +# assert ( +# Path(freqai.dk.data_path / str(freqai.dk.model_filename + "_svm_model.joblib")) +# .resolve() +# .exists() +# ) + +# shutil.rmtree(Path(freqai.dk.full_path)) + + +def test_start_backtesting(mocker, default_conf): + freqaiconf = freqai_conf(copy.deepcopy(default_conf)) + freqaiconf.update({"timerange": "20180120-20180130"}) + strategy = get_patched_freqai_strategy(mocker, freqaiconf) + exchange = get_patched_exchange(mocker, freqaiconf) + strategy.dp = DataProvider(freqaiconf, exchange) + strategy.freqai_info = freqaiconf.get("freqai", {}) + freqai = strategy.model.bridge + freqai.live = False + freqai.dk = FreqaiDataKitchen(freqaiconf, freqai.dd) + timerange = TimeRange.parse_timerange("20180110-20180130") + freqai.dk.load_all_pair_histories(timerange) + sub_timerange = TimeRange.parse_timerange("20180110-20180130") + corr_df, base_df = freqai.dk.get_base_and_corr_dataframes(sub_timerange, "LTC/BTC") + + df = freqai.dk.use_strategy_to_populate_indicators(strategy, corr_df, base_df, "LTC/BTC") + + metadata = {"pair": "ADA/BTC"} + freqai.start_backtesting(df, metadata, freqai.dk) + model_folders = [x for x in freqai.dd.full_path.iterdir() if x.is_dir()] + + assert len(model_folders) == 5 + + shutil.rmtree(Path(freqai.dk.full_path)) + + +def test_start_backtesting_from_existing_folder(mocker, default_conf, caplog): + freqaiconf = freqai_conf(copy.deepcopy(default_conf)) + freqaiconf.update({"timerange": "20180120-20180130"}) + strategy = get_patched_freqai_strategy(mocker, freqaiconf) + exchange = get_patched_exchange(mocker, freqaiconf) + strategy.dp = DataProvider(freqaiconf, exchange) + strategy.freqai_info = freqaiconf.get("freqai", {}) + freqai = strategy.model.bridge + freqai.live = False + freqai.dk = FreqaiDataKitchen(freqaiconf, freqai.dd) + timerange = TimeRange.parse_timerange("20180110-20180130") + freqai.dk.load_all_pair_histories(timerange) + sub_timerange = TimeRange.parse_timerange("20180110-20180130") + corr_df, base_df = freqai.dk.get_base_and_corr_dataframes(sub_timerange, "LTC/BTC") + + df = freqai.dk.use_strategy_to_populate_indicators(strategy, corr_df, base_df, "LTC/BTC") + + metadata = {"pair": "ADA/BTC"} + freqai.start_backtesting(df, metadata, freqai.dk) + model_folders = [x for x in freqai.dd.full_path.iterdir() if x.is_dir()] + + assert len(model_folders) == 5 + + # without deleting the exiting folder structure, re-run + + freqaiconf.update({"timerange": "20180120-20180130"}) + strategy = get_patched_freqai_strategy(mocker, freqaiconf) + exchange = get_patched_exchange(mocker, freqaiconf) + strategy.dp = DataProvider(freqaiconf, exchange) + strategy.freqai_info = freqaiconf.get("freqai", {}) + freqai = strategy.model.bridge + freqai.live = False + freqai.dk = FreqaiDataKitchen(freqaiconf, freqai.dd) + timerange = TimeRange.parse_timerange("20180110-20180130") + freqai.dk.load_all_pair_histories(timerange) + sub_timerange = TimeRange.parse_timerange("20180110-20180130") + corr_df, base_df = freqai.dk.get_base_and_corr_dataframes(sub_timerange, "LTC/BTC") + + df = freqai.dk.use_strategy_to_populate_indicators(strategy, corr_df, base_df, "LTC/BTC") + freqai.start_backtesting(df, metadata, freqai.dk) + + assert log_has_re( + "Found model at ", + caplog, + ) + + shutil.rmtree(Path(freqai.dk.full_path)) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 57c08f48e..86fe0bf03 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -1403,7 +1403,8 @@ def test_api_strategies(botclient): 'StrategyTestV2', 'StrategyTestV3', 'StrategyTestV3Analysis', - 'StrategyTestV3Futures' + 'StrategyTestV3Futures', + 'freqai_test_strat' ]} diff --git a/tests/strategy/strats/freqai_test_strat.py b/tests/strategy/strats/freqai_test_strat.py new file mode 100644 index 000000000..28e3dce54 --- /dev/null +++ b/tests/strategy/strats/freqai_test_strat.py @@ -0,0 +1,182 @@ +import logging +from functools import reduce + +import pandas as pd +import talib.abstract as ta +from pandas import DataFrame + +from freqtrade.freqai.strategy_bridge import CustomModel +from freqtrade.strategy import DecimalParameter, IntParameter, merge_informative_pair +from freqtrade.strategy.interface import IStrategy + + +logger = logging.getLogger(__name__) + + +class freqai_test_strat(IStrategy): + """ + Example strategy showing how the user connects their own + IFreqaiModel to the strategy. Namely, the user uses: + self.model = CustomModel(self.config) + self.model.bridge.start(dataframe, metadata) + + to make predictions on their data. populate_any_indicators() automatically + generates the variety of features indicated by the user in the + canonical freqtrade configuration file under config['freqai']. + """ + + minimal_roi = {"0": 0.1, "240": -1} + + plot_config = { + "main_plot": {}, + "subplots": { + "prediction": {"prediction": {"color": "blue"}}, + "target_roi": { + "target_roi": {"color": "brown"}, + }, + "do_predict": { + "do_predict": {"color": "brown"}, + }, + }, + } + + process_only_new_candles = True + stoploss = -0.05 + use_exit_signal = True + startup_candle_count: int = 300 + 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"] + informative_pairs = [] + for tf in self.config["freqai"]["feature_parameters"]["include_timeframes"]: + for pair in whitelist_pairs: + informative_pairs.append((pair, tf)) + for pair in corr_pairs: + if pair in whitelist_pairs: + continue # avoid duplication + informative_pairs.append((pair, tf)) + return informative_pairs + + def bot_start(self): + self.model = CustomModel(self.config) + + def populate_any_indicators( + self, metadata, pair, df, tf, informative=None, coin="", set_generalized_indicators=False + ): + """ + Function designed to automatically generate, name and merge features + from user indicated timeframes in the configuration file. User controls the indicators + passed to the training/prediction by prepending indicators with `'%-' + coin ` + (see convention below). I.e. user should not prepend any supporting metrics + (e.g. bb_lowerband below) with % unless they explicitly want to pass that metric to the + model. + :params: + :pair: pair to be used as informative + :df: strategy dataframe which will receive merges from informatives + :tf: timeframe of the dataframe which will modify the feature names + :informative: the dataframe associated with the informative pair + :coin: the name of the coin which will modify the feature names. + """ + + with self.model.bridge.lock: + if informative is None: + informative = self.dp.get_pair_dataframe(pair, tf) + + # first loop is automatically duplicating indicators for time periods + for t in self.freqai_info["feature_parameters"]["indicator_periods_candles"]: + + 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}pct-change"] = informative["close"].pct_change() + informative[f"%-{coin}raw_volume"] = informative["volume"] + informative[f"%-{coin}raw_price"] = informative["close"] + + indicators = [col for col in informative if col.startswith("%")] + # This loop duplicates and shifts all indicators to add a sense of recency to data + for n in range(self.freqai_info["feature_parameters"]["include_shifted_candles"] + 1): + if n == 0: + continue + informative_shift = informative[indicators].shift(n) + informative_shift = informative_shift.add_suffix("_shift-" + str(n)) + informative = pd.concat((informative, informative_shift), axis=1) + + df = merge_informative_pair(df, informative, self.config["timeframe"], tf, ffill=True) + skip_columns = [ + (s + "_" + tf) for s in ["date", "open", "high", "low", "close", "volume"] + ] + df = df.drop(columns=skip_columns) + + # Add generalized indicators here (because in live, it will call this + # function to populate indicators during training). Notice how we ensure not to + # add them multiple times + if set_generalized_indicators: + df["%-day_of_week"] = (df["date"].dt.dayofweek + 1) / 7 + df["%-hour_of_day"] = (df["date"].dt.hour + 1) / 25 + + # user adds targets here by prepending them with &- (see convention below) + # If user wishes to use multiple targets, a multioutput prediction model + # needs to be used such as templates/CatboostPredictionMultiModel.py + df["&-s_close"] = ( + df["close"] + .shift(-self.freqai_info["feature_parameters"]["label_period_candles"]) + .rolling(self.freqai_info["feature_parameters"]["label_period_candles"]) + .mean() + / df["close"] + - 1 + ) + + return df + + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + + self.freqai_info = self.config["freqai"] + + # All indicators must be populated by populate_any_indicators() for live functionality + # to work correctly. + # the model will return 4 values, its prediction, an indication of whether or not the + # prediction should be accepted, the target mean/std values from the labels used during + # each training period. + dataframe = self.model.bridge.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 + 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"]] + + 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"]] + + if enter_short_conditions: + df.loc[ + reduce(lambda x, y: x & y, enter_short_conditions), ["enter_short", "enter_tag"] + ] = (1, "short") + + 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] + 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] + if exit_short_conditions: + df.loc[reduce(lambda x, y: x & y, exit_short_conditions), "exit_short"] = 1 + + return df diff --git a/tests/strategy/test_strategy_loading.py b/tests/strategy/test_strategy_loading.py index bdfcf3211..aaad26e5b 100644 --- a/tests/strategy/test_strategy_loading.py +++ b/tests/strategy/test_strategy_loading.py @@ -34,7 +34,7 @@ def test_search_all_strategies_no_failed(): directory = Path(__file__).parent / "strats" strategies = StrategyResolver.search_all_objects(directory, enum_failed=False) assert isinstance(strategies, list) - assert len(strategies) == 7 + assert len(strategies) == 8 assert isinstance(strategies[0], dict) @@ -42,10 +42,10 @@ def test_search_all_strategies_with_failed(): directory = Path(__file__).parent / "strats" strategies = StrategyResolver.search_all_objects(directory, enum_failed=True) assert isinstance(strategies, list) - assert len(strategies) == 8 + assert len(strategies) == 9 # with enum_failed=True search_all_objects() shall find 2 good strategies # and 1 which fails to load - assert len([x for x in strategies if x['class'] is not None]) == 7 + assert len([x for x in strategies if x['class'] is not None]) == 8 assert len([x for x in strategies if x['class'] is None]) == 1 From 263df87313e7cbb87ebadf7e27f0110e8b4513c9 Mon Sep 17 00:00:00 2001 From: longyu Date: Sat, 13 Aug 2022 10:47:16 +0200 Subject: [PATCH 016/126] bugfix pca for test features when test split is null --- 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 f16e169b9..9f07c9e70 100644 --- a/freqtrade/freqai/data_kitchen.py +++ b/freqtrade/freqai/data_kitchen.py @@ -440,7 +440,6 @@ class FreqaiDataKitchen: logger.info("reduced feature dimension by %s", n_components - n_keep_components) logger.info("explained variance %f", np.sum(pca2.explained_variance_ratio_)) train_components = pca2.transform(self.data_dictionary["train_features"]) - test_components = pca2.transform(self.data_dictionary["test_features"]) self.data_dictionary["train_features"] = pd.DataFrame( data=train_components, @@ -454,6 +453,7 @@ class FreqaiDataKitchen: self.training_features_list = self.data_dictionary["train_features"].columns if self.freqai_config.get('data_split_parameters', {}).get('test_size', 0.1) != 0: + test_components = pca2.transform(self.data_dictionary["test_features"]) self.data_dictionary["test_features"] = pd.DataFrame( data=test_components, columns=["PC" + str(i) for i in range(0, n_keep_components)], From 3140559446bf1251e5a1988fa7f7c3bc4eeb7d34 Mon Sep 17 00:00:00 2001 From: longyu Date: Wed, 17 Aug 2022 14:57:38 +0200 Subject: [PATCH 017/126] ignore sample self distance in avg_mean_dist --- freqtrade/freqai/data_kitchen.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/freqtrade/freqai/data_kitchen.py b/freqtrade/freqai/data_kitchen.py index 60b3b782e..421b30bf5 100644 --- a/freqtrade/freqai/data_kitchen.py +++ b/freqtrade/freqai/data_kitchen.py @@ -506,7 +506,10 @@ class FreqaiDataKitchen: # logger.info("computing average mean distance for all training points") pairwise = pairwise_distances( self.data_dictionary["train_features"], n_jobs=self.thread_count) - avg_mean_dist = pairwise.mean(axis=1).mean() + # remove the diagonal distances which are itself distances ~0 + np.fill_diagonal(pairwise, np.NaN) + pairwise = pairwise.reshape(-1, 1) + avg_mean_dist = pairwise[~np.isnan(pairwise)].mean() return avg_mean_dist From 995defbb1848f23cb900ce91ecd8345066b18bba Mon Sep 17 00:00:00 2001 From: longyu Date: Wed, 17 Aug 2022 15:07:06 +0200 Subject: [PATCH 018/126] new line --- freqtrade/freqai/data_kitchen.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/freqai/data_kitchen.py b/freqtrade/freqai/data_kitchen.py index 421b30bf5..85041515a 100644 --- a/freqtrade/freqai/data_kitchen.py +++ b/freqtrade/freqai/data_kitchen.py @@ -511,6 +511,7 @@ class FreqaiDataKitchen: pairwise = pairwise.reshape(-1, 1) avg_mean_dist = pairwise[~np.isnan(pairwise)].mean() + return avg_mean_dist def use_SVM_to_remove_outliers(self, predict: bool) -> None: From 52ee7fc981abf2efc153b52de5dcb151de636744 Mon Sep 17 00:00:00 2001 From: th0rntwig Date: Thu, 18 Aug 2022 14:44:49 +0200 Subject: [PATCH 019/126] 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 35f51baed..7a885659d 100644 --- a/freqtrade/freqai/data_kitchen.py +++ b/freqtrade/freqai/data_kitchen.py @@ -654,6 +654,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 98c62dad910ac74a8579e099d1a07e4cc5b0180c Mon Sep 17 00:00:00 2001 From: robcaulk Date: Thu, 18 Aug 2022 19:15:29 +0200 Subject: [PATCH 020/126] 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 7a885659d..ca4687902 100644 --- a/freqtrade/freqai/data_kitchen.py +++ b/freqtrade/freqai/data_kitchen.py @@ -654,81 +654,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 49e4ce5c3..3535d7371 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.total_pairs = len(self.config.get("exchange", {}).get("pair_whitelist")) self.last_trade_database_summary: DataFrame = {} @@ -389,18 +391,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: @@ -408,6 +412,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. @@ -419,18 +428,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 755041c134989d10093f1d65f23ebe2d45c643fe Mon Sep 17 00:00:00 2001 From: robcaulk Date: Fri, 19 Aug 2022 18:35:24 +0200 Subject: [PATCH 021/126] 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 ca4687902..c8516a8bd 100644 --- a/freqtrade/freqai/data_kitchen.py +++ b/freqtrade/freqai/data_kitchen.py @@ -752,6 +752,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 3535d7371..07303b49f 100644 --- a/freqtrade/freqai/freqai_interface.py +++ b/freqtrade/freqai/freqai_interface.py @@ -385,10 +385,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"] @@ -417,16 +416,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 0c810868de467e8a9a5f5f3941992117abadb31a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 18 Sep 2022 14:59:16 +0200 Subject: [PATCH 022/126] Add Dataprovider to pairlist --- freqtrade/freqtradebot.py | 5 ++++- freqtrade/optimize/backtesting.py | 2 +- freqtrade/plugins/pairlistmanager.py | 6 ++++-- tests/plugins/test_pairlist.py | 10 +++++----- 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 72b88a82f..169af2ab6 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -82,7 +82,10 @@ class FreqtradeBot(LoggingMixin): # Keep this at the end of this initialization method. self.rpc: RPCManager = RPCManager(self) - self.dataprovider = DataProvider(self.config, self.exchange, self.pairlists, self.rpc) + self.dataprovider = DataProvider(self.config, self.exchange, rpc=self.rpc) + self.pairlists = PairListManager(self.exchange, self.config, self.dataprovider) + + self.dataprovider.add_pairlisthandler(self.pairlists) # Attach Dataprovider to strategy instance self.strategy.dp = self.dataprovider diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 2a1c44f7f..aa25e049a 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -110,7 +110,7 @@ class Backtesting: self.timeframe = str(self.config.get('timeframe')) self.timeframe_min = timeframe_to_minutes(self.timeframe) self.init_backtest_detail() - self.pairlists = PairListManager(self.exchange, self.config) + self.pairlists = PairListManager(self.exchange, self.config, self.dataprovider) if 'VolumePairList' in self.pairlists.name_list: raise OperationalException("VolumePairList not allowed for backtesting. " "Please use StaticPairlist instead.") diff --git a/freqtrade/plugins/pairlistmanager.py b/freqtrade/plugins/pairlistmanager.py index e01abb297..763307d3f 100644 --- a/freqtrade/plugins/pairlistmanager.py +++ b/freqtrade/plugins/pairlistmanager.py @@ -3,11 +3,12 @@ PairList manager class """ import logging from functools import partial -from typing import Dict, List +from typing import Dict, List, Optional from cachetools import TTLCache, cached from freqtrade.constants import Config, ListPairsWithTimeframes +from freqtrade.data.dataprovider import DataProvider from freqtrade.enums import CandleType from freqtrade.exceptions import OperationalException from freqtrade.mixins import LoggingMixin @@ -21,13 +22,14 @@ logger = logging.getLogger(__name__) class PairListManager(LoggingMixin): - def __init__(self, exchange, config: Config) -> None: + def __init__(self, exchange, config: Config, dataprovider: DataProvider = None) -> None: self._exchange = exchange self._config = config self._whitelist = self._config['exchange'].get('pair_whitelist') self._blacklist = self._config['exchange'].get('pair_blacklist', []) self._pairlist_handlers: List[IPairList] = [] self._tickers_needed = False + self._dataprovider: Optional[DataProvider] = dataprovider for pairlist_handler_config in self._config.get('pairlists', []): pairlist_handler = PairListResolver.load_pairlist( pairlist_handler_config['method'], diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index 538751251..26b7ebbe2 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -126,7 +126,7 @@ def test_log_cached(mocker, static_pl_conf, markets, tickers): def test_load_pairlist_noexist(mocker, markets, default_conf): freqtrade = get_patched_freqtradebot(mocker, default_conf) mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets)) - plm = PairListManager(freqtrade.exchange, default_conf) + plm = PairListManager(freqtrade.exchange, default_conf, MagicMock()) with pytest.raises(OperationalException, match=r"Impossible to load Pairlist 'NonexistingPairList'. " r"This class does not exist or contains Python code errors."): @@ -137,7 +137,7 @@ def test_load_pairlist_noexist(mocker, markets, default_conf): def test_load_pairlist_verify_multi(mocker, markets_static, default_conf): freqtrade = get_patched_freqtradebot(mocker, default_conf) mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets_static)) - plm = PairListManager(freqtrade.exchange, default_conf) + plm = PairListManager(freqtrade.exchange, default_conf, MagicMock()) # Call different versions one after the other, should always consider what was passed in # and have no side-effects (therefore the same check multiple times) assert plm.verify_whitelist(['ETH/BTC', 'XRP/BTC', ], print) == ['ETH/BTC', 'XRP/BTC'] @@ -269,7 +269,7 @@ def test_refresh_pairlist_dynamic(mocker, shitcoinmarkets, tickers, whitelist_co with pytest.raises(OperationalException, match=r'`number_assets` not specified. Please check your configuration ' r'for "pairlist.config.number_assets"'): - PairListManager(freqtrade.exchange, whitelist_conf) + PairListManager(freqtrade.exchange, whitelist_conf, MagicMock()) def test_refresh_pairlist_dynamic_2(mocker, shitcoinmarkets, tickers, whitelist_conf_2): @@ -694,7 +694,7 @@ def test_PrecisionFilter_error(mocker, whitelist_conf) -> None: with pytest.raises(OperationalException, match=r"PrecisionFilter can only work with stoploss defined\..*"): - PairListManager(MagicMock, whitelist_conf) + PairListManager(MagicMock, whitelist_conf, MagicMock()) def test_PerformanceFilter_error(mocker, whitelist_conf, caplog) -> None: @@ -703,7 +703,7 @@ def test_PerformanceFilter_error(mocker, whitelist_conf, caplog) -> None: del Trade.query mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) exchange = get_patched_exchange(mocker, whitelist_conf) - pm = PairListManager(exchange, whitelist_conf) + pm = PairListManager(exchange, whitelist_conf, MagicMock()) pm.refresh_pairlist() assert log_has("PerformanceFilter is not available in this mode.", caplog) From 4940fa7be3520e6096ccbd6f7231d0eb6b9b128b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 18 Sep 2022 15:29:57 +0200 Subject: [PATCH 023/126] Add Producer Pairlist --- freqtrade/constants.py | 2 +- .../plugins/pairlist/ProducerPairList.py | 87 +++++++++++++++++++ 2 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 freqtrade/plugins/pairlist/ProducerPairList.py diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 4c2bd6e18..e0e42c821 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -31,7 +31,7 @@ HYPEROPT_LOSS_BUILTIN = ['ShortTradeDurHyperOptLoss', 'OnlyProfitHyperOptLoss', 'CalmarHyperOptLoss', 'MaxDrawDownHyperOptLoss', 'MaxDrawDownRelativeHyperOptLoss', 'ProfitDrawDownHyperOptLoss'] -AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', +AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', 'ProducerPairList', 'AgeFilter', 'OffsetFilter', 'PerformanceFilter', 'PrecisionFilter', 'PriceFilter', 'RangeStabilityFilter', 'ShuffleFilter', 'SpreadFilter', 'VolatilityFilter'] diff --git a/freqtrade/plugins/pairlist/ProducerPairList.py b/freqtrade/plugins/pairlist/ProducerPairList.py new file mode 100644 index 000000000..81320f713 --- /dev/null +++ b/freqtrade/plugins/pairlist/ProducerPairList.py @@ -0,0 +1,87 @@ +""" +External Pair List provider + +Provides pair list from Leader data +""" +import logging +from typing import Any, Dict, List, Optional + +from freqtrade.plugins.pairlist.IPairList import IPairList + + +logger = logging.getLogger(__name__) + + +class ProducerPairList(IPairList): + """ + PairList plugin for use with external_message_consumer. + Will use pairs given from leader data. + + Usage: + "pairlists": [ + { + "method": "ProducerPairList", + "number_assets": 5, + "producer_name": "default", + } + ], + """ + + def __init__(self, exchange, pairlistmanager, + config: Dict[str, Any], pairlistconfig: Dict[str, Any], + pairlist_pos: int) -> None: + super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos) + + self._num_assets = self._pairlistconfig.get('number_assets') + self._producer_name = self._pairlistconfig.get('producer_name', 'default') + if config.get('external_message_consumer').get('enabled') is False: + raise ValueError("ProducerPairList requires external_message_consumer to be enabled.") + + @property + def needstickers(self) -> bool: + """ + Boolean property defining if tickers are necessary. + If no Pairlist requires tickers, an empty Dict is passed + as tickers argument to filter_pairlist + """ + return False + + def short_desc(self) -> str: + """ + Short whitelist method description - used for startup-messages + -> Please overwrite in subclasses + """ + return f"{self.name} - {self._producer_name}" + + def _filter_pairlist(self, pairlist: Optional[List[str]]): + upstream_pairlist = self._pairlistmanager._dataprovider.get_producer_pairs( + self._producer_name) + + if pairlist is None: + pairlist = self._pairlistmanager._dataprovider.get_producer_pairs(self._producer_name) + + pairs = list(dict.fromkeys(upstream_pairlist + pairlist))[:self._num_assets] + + return pairs + + def gen_pairlist(self, tickers: Dict) -> List[str]: + """ + Generate the pairlist + :param tickers: Tickers (from exchange.get_tickers()). May be cached. + :return: List of pairs + """ + pairs = self._filter_pairlist(None) + self.log_once(f"Received pairs: {pairs}", logger.debug) + pairs = self._whitelist_for_active_markets(self.verify_whitelist(pairs, logger.info)) + self.log_once(f"New Pairlist: {pairs}", logger.info) + return pairs + + def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]: + """ + Filters and sorts pairlist and returns the whitelist again. + Called on each bot iteration - please use internal caching if necessary + :param pairlist: pairlist to filter or sort + :param tickers: Tickers (from exchange.get_tickers()). May be cached. + :return: new whitelist + """ + return self._filter_pairlist(pairlist) From 527fd36134f16995ebf960a42fff1080100e8af7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 25 Sep 2022 09:38:20 +0200 Subject: [PATCH 024/126] num_assets should be optional --- freqtrade/plugins/pairlist/ProducerPairList.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/freqtrade/plugins/pairlist/ProducerPairList.py b/freqtrade/plugins/pairlist/ProducerPairList.py index 81320f713..0dc90ac0f 100644 --- a/freqtrade/plugins/pairlist/ProducerPairList.py +++ b/freqtrade/plugins/pairlist/ProducerPairList.py @@ -32,7 +32,7 @@ class ProducerPairList(IPairList): pairlist_pos: int) -> None: super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos) - self._num_assets = self._pairlistconfig.get('number_assets') + self._num_assets: int = self._pairlistconfig.get('number_assets', 0) self._producer_name = self._pairlistconfig.get('producer_name', 'default') if config.get('external_message_consumer').get('enabled') is False: raise ValueError("ProducerPairList requires external_message_consumer to be enabled.") @@ -60,7 +60,9 @@ class ProducerPairList(IPairList): if pairlist is None: pairlist = self._pairlistmanager._dataprovider.get_producer_pairs(self._producer_name) - pairs = list(dict.fromkeys(upstream_pairlist + pairlist))[:self._num_assets] + pairs = list(dict.fromkeys(upstream_pairlist + pairlist)) + if self._num_assets: + pairs = pairs[:self._num_assets] return pairs From 1c089dcd51fb192ed41beb98573cd915c2be380e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 25 Sep 2022 09:40:44 +0200 Subject: [PATCH 025/126] Add docs for Producer/consumer pairlist --- docs/includes/pairlists.md | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/docs/includes/pairlists.md b/docs/includes/pairlists.md index 0f55c1b79..7dff75a02 100644 --- a/docs/includes/pairlists.md +++ b/docs/includes/pairlists.md @@ -22,6 +22,7 @@ You may also use something like `.*DOWN/BTC` or `.*UP/BTC` to exclude leveraged * [`StaticPairList`](#static-pair-list) (default, if not configured differently) * [`VolumePairList`](#volume-pair-list) +* [`ProducerPairList`](#producerpairlist) * [`AgeFilter`](#agefilter) * [`OffsetFilter`](#offsetfilter) * [`PerformanceFilter`](#performancefilter) @@ -84,7 +85,7 @@ Filtering instances (not the first position in the list) will not apply any cach You can define a minimum volume with `min_value` - which will filter out pairs with a volume lower than the specified value in the specified timerange. -### VolumePairList Advanced mode +##### VolumePairList Advanced mode `VolumePairList` can also operate in an advanced mode to build volume over a given timerange of specified candle size. It utilizes exchange historical candle data, builds a typical price (calculated by (open+high+low)/3) and multiplies the typical price with every candle's volume. The sum is the `quoteVolume` over the given range. This allows different scenarios, for a more smoothened volume, when using longer ranges with larger candle sizes, or the opposite when using a short range with small candles. @@ -146,6 +147,32 @@ More sophisticated approach can be used, by using `lookback_timeframe` for candl !!! Note `VolumePairList` does not support backtesting mode. +#### ProducerPairList + +With `ProducerPairList`, you can reuse the pairlist from a [Producer](producer-consumer.md) without explicitly defining the pairlist on each consumer. + +[Consumer mode](producer-consumer.md) is required for this pairlist to work. + +The pairlist will perform a check on active pairs against the current exchange configuration to avoid attempting to trade on invalid markets. + +You can limit the length of the pairlist with the optional parameter `number_assets`. Using `"number_assets"=0` or omitting this key will result in the reuse of all producer pairs valid for the current setup. + +```json +"pairlists": [ + { + "method": "ProducerPairList", + "number_assets": 5, + "producer_name": "default", + } +], +``` + + +!!! Tip "Combining pairlists" + This pairlist can be combined with all other pairlists and filters for further pairlist reduction, and can also act as an "additional" pairlist, on top of already defined pairs. + `ProducerPairList` can also be used multiple times in sequence, combining the pairs from multiple producers. + Obviously in complex such configurations, the Producer may not provide data for all pairs, so the strategy must be fit for this. + #### AgeFilter Removes pairs that have been listed on the exchange for less than `min_days_listed` days (defaults to `10`) or more than `max_days_listed` days (defaults `None` mean infinity). From 30d51b6939819b541b9582284b93d17bc6927783 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 25 Sep 2022 09:43:39 +0200 Subject: [PATCH 026/126] Move "pairlist" logging to manager --- freqtrade/plugins/pairlist/ProducerPairList.py | 1 - freqtrade/plugins/pairlist/VolumePairList.py | 2 -- freqtrade/plugins/pairlistmanager.py | 2 ++ 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/freqtrade/plugins/pairlist/ProducerPairList.py b/freqtrade/plugins/pairlist/ProducerPairList.py index 0dc90ac0f..d0fb4ada2 100644 --- a/freqtrade/plugins/pairlist/ProducerPairList.py +++ b/freqtrade/plugins/pairlist/ProducerPairList.py @@ -75,7 +75,6 @@ class ProducerPairList(IPairList): pairs = self._filter_pairlist(None) self.log_once(f"Received pairs: {pairs}", logger.debug) pairs = self._whitelist_for_active_markets(self.verify_whitelist(pairs, logger.info)) - self.log_once(f"New Pairlist: {pairs}", logger.info) return pairs def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]: diff --git a/freqtrade/plugins/pairlist/VolumePairList.py b/freqtrade/plugins/pairlist/VolumePairList.py index 9dcada291..b290f76aa 100644 --- a/freqtrade/plugins/pairlist/VolumePairList.py +++ b/freqtrade/plugins/pairlist/VolumePairList.py @@ -232,6 +232,4 @@ class VolumePairList(IPairList): # Limit pairlist to the requested number of pairs pairs = pairs[:self._number_pairs] - self.log_once(f"Searching {self._number_pairs} pairs: {pairs}", logger.info) - return pairs diff --git a/freqtrade/plugins/pairlistmanager.py b/freqtrade/plugins/pairlistmanager.py index 763307d3f..5ed319e93 100644 --- a/freqtrade/plugins/pairlistmanager.py +++ b/freqtrade/plugins/pairlistmanager.py @@ -98,6 +98,8 @@ class PairListManager(LoggingMixin): # to ensure blacklist is respected. pairlist = self.verify_blacklist(pairlist, logger.warning) + self.log_once(f"Whitelist with {len(pairlist)} pairs: {pairlist}", logger.info) + self._whitelist = pairlist def verify_blacklist(self, pairlist: List[str], logmethod) -> List[str]: From 1bb45a2650df119a5dad27b37a0390584254655e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 25 Sep 2022 09:47:57 +0200 Subject: [PATCH 027/126] Fix crash due to insufficient check --- freqtrade/plugins/pairlist/ProducerPairList.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/plugins/pairlist/ProducerPairList.py b/freqtrade/plugins/pairlist/ProducerPairList.py index d0fb4ada2..dc575f29b 100644 --- a/freqtrade/plugins/pairlist/ProducerPairList.py +++ b/freqtrade/plugins/pairlist/ProducerPairList.py @@ -34,7 +34,7 @@ class ProducerPairList(IPairList): self._num_assets: int = self._pairlistconfig.get('number_assets', 0) self._producer_name = self._pairlistconfig.get('producer_name', 'default') - if config.get('external_message_consumer').get('enabled') is False: + if config.get('external_message_consumer', {}).get('enabled') is False: raise ValueError("ProducerPairList requires external_message_consumer to be enabled.") @property From bd106b4b8eaf581b0a77a31e1d1f8133ba067b9a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 25 Sep 2022 10:13:00 +0200 Subject: [PATCH 028/126] Add tests for Producerpairlist --- .../plugins/pairlist/ProducerPairList.py | 6 +- tests/conftest.py | 2 + tests/plugins/test_pairlist.py | 72 +++++++++++++++++++ 3 files changed, 78 insertions(+), 2 deletions(-) diff --git a/freqtrade/plugins/pairlist/ProducerPairList.py b/freqtrade/plugins/pairlist/ProducerPairList.py index dc575f29b..fa351c9cc 100644 --- a/freqtrade/plugins/pairlist/ProducerPairList.py +++ b/freqtrade/plugins/pairlist/ProducerPairList.py @@ -6,6 +6,7 @@ Provides pair list from Leader data import logging from typing import Any, Dict, List, Optional +from freqtrade.exceptions import OperationalException from freqtrade.plugins.pairlist.IPairList import IPairList @@ -34,8 +35,9 @@ class ProducerPairList(IPairList): self._num_assets: int = self._pairlistconfig.get('number_assets', 0) self._producer_name = self._pairlistconfig.get('producer_name', 'default') - if config.get('external_message_consumer', {}).get('enabled') is False: - raise ValueError("ProducerPairList requires external_message_consumer to be enabled.") + if not config.get('external_message_consumer', {}).get('enabled'): + raise OperationalException( + "ProducerPairList requires external_message_consumer to be enabled.") @property def needstickers(self) -> bool: diff --git a/tests/conftest.py b/tests/conftest.py index 51b1b03e3..a9eeb481e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -200,6 +200,8 @@ def patch_freqtradebot(mocker, config) -> None: mocker.patch('freqtrade.freqtradebot.RPCManager._init', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager.send_msg', MagicMock()) patch_whitelist(mocker, config) + mocker.patch('freqtrade.freqtradebot.ExternalMessageConsumer') + mocker.patch('freqtrade.configuration.config_validation._validate_consumers') def get_patched_freqtradebot(mocker, config) -> FreqtradeBot: diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index 26b7ebbe2..a6b5813da 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -9,6 +9,7 @@ import pytest import time_machine from freqtrade.constants import AVAILABLE_PAIRLISTS +from freqtrade.data.dataprovider import DataProvider from freqtrade.enums import CandleType, RunMode from freqtrade.exceptions import OperationalException from freqtrade.persistence import Trade @@ -40,6 +41,12 @@ def whitelist_conf(default_conf): "sort_key": "quoteVolume", }, ] + default_conf.update({ + "external_message_consumer": { + "enabled": True, + "producers": [], + } + }) return default_conf @@ -1167,6 +1174,10 @@ def test_spreadfilter_invalid_data(mocker, default_conf, markets, tickers, caplo "[{'OffsetFilter': 'OffsetFilter - Taking 10 Pairs, starting from 5.'}]", None ), + ({"method": "ProducerPairList"}, + "[{'ProducerPairList': 'ProducerPairList - default'}]", + None + ), ]) def test_pricefilter_desc(mocker, whitelist_conf, markets, pairlistconfig, desc_expected, exception_expected): @@ -1341,3 +1352,64 @@ def test_expand_pairlist_keep_invalid(wildcardlist, pairs, expected): expand_pairlist(wildcardlist, pairs, keep_invalid=True) else: assert sorted(expand_pairlist(wildcardlist, pairs, keep_invalid=True)) == sorted(expected) + + +def test_ProducerPairlist_no_emc(mocker, whitelist_conf): + mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) + + whitelist_conf['pairlists'] = [ + { + "method": "ProducerPairList", + "number_assets": 10, + "producer_name": "hello_world", + } + ] + del whitelist_conf['external_message_consumer'] + + with pytest.raises(OperationalException, + match=r"ProducerPairList requires external_message_consumer to be enabled."): + get_patched_freqtradebot(mocker, whitelist_conf) + + +def test_ProducerPairlist(mocker, whitelist_conf, markets): + mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) + mocker.patch.multiple('freqtrade.exchange.Exchange', + markets=PropertyMock(return_value=markets), + exchange_has=MagicMock(return_value=True), + ) + whitelist_conf['pairlists'] = [ + { + "method": "ProducerPairList", + "number_assets": 2, + "producer_name": "hello_world", + } + ] + whitelist_conf.update({ + "external_message_consumer": { + "enabled": True, + "producers": [ + { + "name": "hello_world", + "host": "null", + "port": 9891, + "ws_token": "dummy", + } + ] + } + }) + + exchange = get_patched_exchange(mocker, whitelist_conf) + dp = DataProvider(whitelist_conf, exchange, None) + pairs = ['ETH/BTC', 'LTC/BTC', 'XRP/BTC'] + # different producer + dp._set_producer_pairs(pairs + ['MEEP/USDT'], 'default') + pm = PairListManager(exchange, whitelist_conf, dp) + pm.refresh_pairlist() + assert pm.whitelist == [] + # proper producer + dp._set_producer_pairs(pairs, 'hello_world') + pm.refresh_pairlist() + + # Pairlist reduced to 2 + assert pm.whitelist == pairs[:2] + assert len(pm.whitelist) == 2 From af59572cb9a7abd5286540de3dc3a38fb355cc64 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 25 Sep 2022 19:32:39 +0200 Subject: [PATCH 029/126] prior pairlists should go first --- freqtrade/plugins/pairlist/ProducerPairList.py | 2 +- tests/plugins/test_pairlist.py | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/freqtrade/plugins/pairlist/ProducerPairList.py b/freqtrade/plugins/pairlist/ProducerPairList.py index fa351c9cc..50b674e60 100644 --- a/freqtrade/plugins/pairlist/ProducerPairList.py +++ b/freqtrade/plugins/pairlist/ProducerPairList.py @@ -62,7 +62,7 @@ class ProducerPairList(IPairList): if pairlist is None: pairlist = self._pairlistmanager._dataprovider.get_producer_pairs(self._producer_name) - pairs = list(dict.fromkeys(upstream_pairlist + pairlist)) + pairs = list(dict.fromkeys(pairlist + upstream_pairlist)) if self._num_assets: pairs = pairs[:self._num_assets] diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index a6b5813da..82fc99d7a 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -1413,3 +1413,16 @@ def test_ProducerPairlist(mocker, whitelist_conf, markets): # Pairlist reduced to 2 assert pm.whitelist == pairs[:2] assert len(pm.whitelist) == 2 + whitelist_conf['exchange']['pair_whitelist'] = ['TKN/BTC'] + + whitelist_conf['pairlists'] = [ + {"method": "StaticPairList"}, + { + "method": "ProducerPairList", + "producer_name": "hello_world", + } + ] + pm = PairListManager(exchange, whitelist_conf, dp) + pm.refresh_pairlist() + assert len(pm.whitelist) == 4 + assert pm.whitelist == ['TKN/BTC'] + pairs From 8eda3a45a3e91a1dc2720ca5d4b5f9a4301836e9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 27 Sep 2022 19:47:49 +0200 Subject: [PATCH 030/126] Test backest detail with leverage --- .../test_backtesting_adjust_position.py | 21 ++++++++++++------- tests/test_integration.py | 17 +++++++++------ 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/tests/optimize/test_backtesting_adjust_position.py b/tests/optimize/test_backtesting_adjust_position.py index 71f8cdcea..99c160a40 100644 --- a/tests/optimize/test_backtesting_adjust_position.py +++ b/tests/optimize/test_backtesting_adjust_position.py @@ -93,11 +93,16 @@ def test_backtest_position_adjustment(default_conf, fee, mocker, testdatadir) -> t["close_rate"], 6) < round(ln.iloc[0]["high"], 6)) -def test_backtest_position_adjustment_detailed(default_conf, fee, mocker) -> None: +@pytest.mark.parametrize('leverage', [ + 1, 2 +]) +def test_backtest_position_adjustment_detailed(default_conf, fee, mocker, leverage) -> None: default_conf['use_exit_signal'] = False mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=10) mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf')) + mocker.patch("freqtrade.exchange.Exchange.get_max_leverage", return_value=10) + patch_exchange(mocker) default_conf.update({ "stake_amount": 100.0, @@ -105,6 +110,7 @@ def test_backtest_position_adjustment_detailed(default_conf, fee, mocker) -> Non "strategy": "StrategyTestV3" }) backtesting = Backtesting(default_conf) + backtesting._can_short = True backtesting._set_strategy(backtesting.strategylist[0]) pair = 'XRP/USDT' row = [ @@ -120,18 +126,19 @@ def test_backtest_position_adjustment_detailed(default_conf, fee, mocker) -> Non '', # enter_tag '', # exit_tag ] + backtesting.strategy.leverage = MagicMock(return_value=leverage) trade = backtesting._enter_trade(pair, row=row, direction='long') trade.orders[0].close_bt_order(row[0], trade) assert trade assert pytest.approx(trade.stake_amount) == 100.0 - assert pytest.approx(trade.amount) == 47.61904762 + assert pytest.approx(trade.amount) == 47.61904762 * leverage assert len(trade.orders) == 1 backtesting.strategy.adjust_trade_position = MagicMock(return_value=None) trade = backtesting._get_adjust_trade_entry_for_candle(trade, row) assert trade assert pytest.approx(trade.stake_amount) == 100.0 - assert pytest.approx(trade.amount) == 47.61904762 + assert pytest.approx(trade.amount) == 47.61904762 * leverage assert len(trade.orders) == 1 # Increase position by 100 backtesting.strategy.adjust_trade_position = MagicMock(return_value=100) @@ -140,7 +147,7 @@ def test_backtest_position_adjustment_detailed(default_conf, fee, mocker) -> Non assert trade assert pytest.approx(trade.stake_amount) == 200.0 - assert pytest.approx(trade.amount) == 95.23809524 + assert pytest.approx(trade.amount) == 95.23809524 * leverage assert len(trade.orders) == 2 # Reduce by more than amount - no change to trade. @@ -150,7 +157,7 @@ def test_backtest_position_adjustment_detailed(default_conf, fee, mocker) -> Non assert trade assert pytest.approx(trade.stake_amount) == 200.0 - assert pytest.approx(trade.amount) == 95.23809524 + assert pytest.approx(trade.amount) == 95.23809524 * leverage assert len(trade.orders) == 2 assert trade.nr_of_successful_entries == 2 @@ -160,7 +167,7 @@ def test_backtest_position_adjustment_detailed(default_conf, fee, mocker) -> Non assert trade assert pytest.approx(trade.stake_amount) == 100.0 - assert pytest.approx(trade.amount) == 47.61904762 + assert pytest.approx(trade.amount) == 47.61904762 * leverage assert len(trade.orders) == 3 assert trade.nr_of_successful_entries == 2 assert trade.nr_of_successful_exits == 1 @@ -171,7 +178,7 @@ def test_backtest_position_adjustment_detailed(default_conf, fee, mocker) -> Non assert trade assert pytest.approx(trade.stake_amount) == 100.0 - assert pytest.approx(trade.amount) == 47.61904762 + assert pytest.approx(trade.amount) == 47.61904762 * leverage assert len(trade.orders) == 3 assert trade.nr_of_successful_entries == 2 assert trade.nr_of_successful_exits == 1 diff --git a/tests/test_integration.py b/tests/test_integration.py index a7b4fbdd3..a848de5d3 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -2,7 +2,7 @@ from unittest.mock import MagicMock import pytest -from freqtrade.enums import ExitCheckTuple, ExitType +from freqtrade.enums import ExitCheckTuple, ExitType, TradingMode from freqtrade.persistence import Trade from freqtrade.persistence.models import Order from freqtrade.rpc.rpc import RPC @@ -455,10 +455,12 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, fee, mocker) -> None: assert pytest.approx(trade.orders[-1].amount) == 61.538461232 -def test_dca_exiting(default_conf_usdt, ticker_usdt, fee, mocker, caplog) -> None: +@pytest.mark.parametrize('leverage', [1, 2]) +def test_dca_exiting(default_conf_usdt, ticker_usdt, fee, mocker, caplog, leverage) -> None: default_conf_usdt['position_adjustment_enable'] = True freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) + freqtrade.trading_mode = TradingMode.FUTURES mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker_usdt, @@ -467,15 +469,17 @@ def test_dca_exiting(default_conf_usdt, ticker_usdt, fee, mocker, caplog) -> Non price_to_precision=lambda s, x, y: y, get_min_pair_stake_amount=MagicMock(return_value=10), ) + mocker.patch("freqtrade.exchange.Exchange.get_max_leverage", return_value=10) patch_get_signal(freqtrade) + freqtrade.strategy.leverage = MagicMock(return_value=leverage) freqtrade.enter_positions() assert len(Trade.get_trades().all()) == 1 trade = Trade.get_trades().first() assert len(trade.orders) == 1 assert pytest.approx(trade.stake_amount) == 60 - assert pytest.approx(trade.amount) == 30.0 + assert pytest.approx(trade.amount) == 30.0 * leverage assert trade.open_rate == 2.0 # Too small size @@ -484,8 +488,9 @@ def test_dca_exiting(default_conf_usdt, ticker_usdt, fee, mocker, caplog) -> Non trade = Trade.get_trades().first() 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 smaller than the minimum of 10.", caplog) + assert pytest.approx(trade.amount) == 30.0 * leverage + assert log_has_re( + r"Remaining amount of \d\.\d+.* would be smaller than the minimum of 10.", caplog) freqtrade.strategy.adjust_trade_position = MagicMock(return_value=-20) @@ -494,7 +499,7 @@ def test_dca_exiting(default_conf_usdt, ticker_usdt, fee, mocker, caplog) -> Non assert len(trade.orders) == 2 assert trade.orders[-1].ft_order_side == 'sell' assert pytest.approx(trade.stake_amount) == 40.198 - assert pytest.approx(trade.amount) == 20.099 + assert pytest.approx(trade.amount) == 20.099 * leverage assert trade.open_rate == 2.0 assert trade.is_open caplog.clear() From 30a5bb08ddcc0708a3a50217243608a294174f20 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 27 Sep 2022 19:53:55 +0200 Subject: [PATCH 031/126] partial exits should account for leverage --- freqtrade/freqtradebot.py | 2 +- freqtrade/optimize/backtesting.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 72b88a82f..b1c95a721 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -597,7 +597,7 @@ class FreqtradeBot(LoggingMixin): # We should decrease our position amount = self.exchange.amount_to_contract_precision( trade.pair, - abs(float(FtPrecise(stake_amount) / FtPrecise(current_exit_rate)))) + abs(float(FtPrecise(stake_amount * trade.leverage) / 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 - diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index e942bdfeb..efe199bdf 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -540,7 +540,7 @@ class Backtesting: if stake_amount is not None and stake_amount < 0.0: amount = amount_to_contract_precision( - abs(stake_amount) / current_rate, trade.amount_precision, + abs(stake_amount * trade.leverage) / current_rate, trade.amount_precision, self.precision_mode, trade.contract_size) if amount == 0.0: return trade From 255c748ca2ac6c4ee58452c8f0e17f077afd2a11 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 27 Sep 2022 19:55:17 +0200 Subject: [PATCH 032/126] Update docs for new trade_position behavior --- docs/strategy-callbacks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index 0b8403414..ea10fc472 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -643,7 +643,7 @@ This callback is **not** called when there is an open order (either buy or sell) Additional Buys are ignored once you have reached the maximum amount of extra buys that you have set on `max_entry_position_adjustment`, but the callback is called anyway looking for partial exits. -Position adjustments will always be applied in the direction of the trade, so a positive value will always increase your position (negative values will decrease your position), no matter if it's a long or short trade. Modifications to leverage are not possible. +Position adjustments will always be applied in the direction of the trade, so a positive value will always increase your position (negative values will decrease your position), no matter if it's a long or short trade. Modifications to leverage are not possible, and the stake-amount is assumed to be before applying leverage. !!! Note "About stake size" Using fixed stake size means it will be the amount used for the first order, just like without position adjustment. From 683b084323d45e0647327756e71ed4ccd4f1d6dd Mon Sep 17 00:00:00 2001 From: th0rntwig Date: Wed, 28 Sep 2022 18:23:56 +0200 Subject: [PATCH 033/126] Set train-test-split shuffle=False as default and remove stratification --- docs/freqai-parameter-table.md | 3 +-- docs/freqai-running.md | 17 ----------------- freqtrade/freqai/data_kitchen.py | 12 +++--------- 3 files changed, 4 insertions(+), 28 deletions(-) diff --git a/docs/freqai-parameter-table.md b/docs/freqai-parameter-table.md index 5969f43c6..c4d044ba4 100644 --- a/docs/freqai-parameter-table.md +++ b/docs/freqai-parameter-table.md @@ -27,8 +27,7 @@ Mandatory parameters are marked as **Required** and have to be set in one of the | `weight_factor` | Weight training data points according to their recency (see details [here](freqai-feature-engineering.md#weighting-features-for-temporal-importance)).
**Datatype:** Positive float (typically < 1). | `indicator_max_period_candles` | **No longer used (#7325)**. Replaced by `startup_candle_count` which is set in the [strategy](freqai-configuration.md#building-a-freqai-strategy). `startup_candle_count` is timeframe independent and defines the maximum *period* used in `populate_any_indicators()` for indicator creation. `FreqAI` uses this parameter together with the maximum timeframe in `include_time_frames` to calculate how many data points to download such that the first data point does not include a NaN
**Datatype:** Positive integer. | `indicator_periods_candles` | Time periods to calculate indicators for. The indicators are added to the base indicator dataset.
**Datatype:** List of positive integers. -| `stratify_training_data` | Split the feature set into training and testing datasets. For example, `stratify_training_data: 2` would set every 2nd data point into a separate dataset to be pulled from during training/testing. See details about how it works [here](freqai-running.md#data-stratification-for-training-and-testing-the-model).
**Datatype:** Positive integer. -| `principal_component_analysis` | Automatically reduce the dimensionality of the data set using Principal Component Analysis. See details about how it works [here](#reducing-data-dimensionality-with-principal-component-analysis)
**Datatype:** Boolean. defaults to `false`. +| `principal_component_analysis` | Automatically reduce the dimensionality of the data set using Principal Component Analysis. See details about how it works [here](#reducing-data-dimensionality-with-principal-component-analysis)
**Datatype:** Boolean. defaults to `False`. | `plot_feature_importances` | Create a feature importance plot for each model for the top/bottom `plot_feature_importances` number of features.
**Datatype:** Integer, defaults to `0`. | `DI_threshold` | Activates the use of the Dissimilarity Index for outlier detection when set to > 0. See details about how it works [here](freqai-feature-engineering.md#identifying-outliers-with-the-dissimilarity-index-di).
**Datatype:** Positive float (typically < 1). | `use_SVM_to_remove_outliers` | Train a support vector machine to detect and remove outliers from the training dataset, as well as from incoming data points. See details about how it works [here](freqai-feature-engineering.md#identifying-outliers-using-a-support-vector-machine-svm).
**Datatype:** Boolean. diff --git a/docs/freqai-running.md b/docs/freqai-running.md index 6c7b56da1..bfefe88c2 100644 --- a/docs/freqai-running.md +++ b/docs/freqai-running.md @@ -105,23 +105,6 @@ During dry/live mode, FreqAI trains each coin pair sequentially (on separate thr In the presented example config, the user will only allow predictions on models that are less than 1/2 hours old. -## Data stratification for training and testing the model - -You can stratify (group) the training/testing data using: - -```json - "freqai": { - "feature_parameters" : { - "stratify_training_data": 3 - } - } -``` - -This will split the data chronologically so that every Xth data point is used to test the model after training. In the example above, the user is asking for every third data point in the dataframe to be used for -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 does not capture the complexity of the data, the test data is significantly different from the train data, or a different type of model should be used. - ## Controlling the model learning process Model training parameters are unique to the selected machine learning library. FreqAI allows you to set any parameter for any library using the `model_training_parameters` dictionary in the config. The example config (found in `config_examples/config_freqai.example.json`) shows some of the example parameters associated with `Catboost` and `LightGBM`, but you can add any parameters available in those libraries or any other machine learning library you choose to implement. diff --git a/freqtrade/freqai/data_kitchen.py b/freqtrade/freqai/data_kitchen.py index f4fa4e5fd..9e22667f3 100644 --- a/freqtrade/freqai/data_kitchen.py +++ b/freqtrade/freqai/data_kitchen.py @@ -134,20 +134,14 @@ class FreqaiDataKitchen: """ feat_dict = self.freqai_config["feature_parameters"] + shuffle = self.freqai_config.get('data_split_parameters', {}).get('shuffle', False) + weights: npt.ArrayLike if feat_dict.get("weight_factor", 0) > 0: weights = self.set_weights_higher_recent(len(filtered_dataframe)) else: weights = np.ones(len(filtered_dataframe)) - if feat_dict.get("stratify_training_data", 0) > 0: - stratification = np.zeros(len(filtered_dataframe)) - for i in range(1, len(stratification)): - if i % feat_dict.get("stratify_training_data", 0) == 0: - stratification[i] = 1 - else: - stratification = None - if self.freqai_config.get('data_split_parameters', {}).get('test_size', 0.1) != 0: ( train_features, @@ -160,7 +154,7 @@ class FreqaiDataKitchen: filtered_dataframe[: filtered_dataframe.shape[0]], labels, weights, - stratify=stratification, + shuffle=shuffle, **self.config["freqai"]["data_split_parameters"], ) else: From 772abfc6f033aec44c414cf3d183485bd5b5979c Mon Sep 17 00:00:00 2001 From: th0rntwig Date: Wed, 28 Sep 2022 19:29:02 +0200 Subject: [PATCH 034/126] Add default value for shuffle in docs --- docs/freqai-parameter-table.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/freqai-parameter-table.md b/docs/freqai-parameter-table.md index c4d044ba4..8e19226ba 100644 --- a/docs/freqai-parameter-table.md +++ b/docs/freqai-parameter-table.md @@ -40,7 +40,7 @@ Mandatory parameters are marked as **Required** and have to be set in one of the | | **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` | The 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`.
**Datatype:** Boolean. +| `shuffle` | Shuffle the training data points during training. Typically, to not remove the chronological order of data in time-series forecasting, this is set to `False`.
**Datatype:** Boolean.
Defaut: `False`. | | **Model training parameters** | `model_training_parameters` | A flexible dictionary that includes all parameters available by the selected model library. For example, if you use `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 you select 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. From 4e920e9c5381dd7d54e9e5c0d400cb1a058b4cf4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 29 Sep 2022 06:41:16 +0200 Subject: [PATCH 035/126] Reduce verbosity of sending-message --- freqtrade/rpc/rpc_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/rpc/rpc_manager.py b/freqtrade/rpc/rpc_manager.py index e286487ff..e3b31d225 100644 --- a/freqtrade/rpc/rpc_manager.py +++ b/freqtrade/rpc/rpc_manager.py @@ -67,7 +67,7 @@ class RPCManager: 'status': 'stopping bot' } """ - if msg.get('type') is not RPCMessageType.ANALYZED_DF: + if msg.get('type') not in (RPCMessageType.ANALYZED_DF, RPCMessageType.WHITELIST): logger.info('Sending rpc message: %s', msg) if 'pair' in msg: msg.update({ From ac229b7a429c185c1b52ca01d21c03414cd0dcc5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 29 Sep 2022 07:10:00 +0200 Subject: [PATCH 036/126] Reduce message consumer verbosity --- freqtrade/rpc/external_message_consumer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/rpc/external_message_consumer.py b/freqtrade/rpc/external_message_consumer.py index dcfe1d109..f5ba4b490 100644 --- a/freqtrade/rpc/external_message_consumer.py +++ b/freqtrade/rpc/external_message_consumer.py @@ -284,7 +284,7 @@ class ExternalMessageConsumer: logger.error(f"Empty message received from `{producer_name}`") return - logger.info(f"Received message of type `{producer_message.type}` from `{producer_name}`") + logger.debug(f"Received message of type `{producer_message.type}` from `{producer_name}`") message_handler = self._message_handlers.get(producer_message.type) From 388a572cb38791f48c0f85cff396ee8c1e1df3bf Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 29 Sep 2022 07:17:38 +0200 Subject: [PATCH 037/126] Version bump develop version --- freqtrade/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index 77c305c66..1e62266a8 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,5 +1,5 @@ """ Freqtrade bot """ -__version__ = '2022.9.dev' +__version__ = '2022.10.dev' if 'dev' in __version__: try: From 80d0e66b48a2aa35d1ca50cc44a73da5fd6bf4ae Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 29 Sep 2022 07:19:16 +0200 Subject: [PATCH 038/126] Update log level in test --- tests/rpc/test_rpc_emc.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/rpc/test_rpc_emc.py b/tests/rpc/test_rpc_emc.py index 2649c5460..28adc66b9 100644 --- a/tests/rpc/test_rpc_emc.py +++ b/tests/rpc/test_rpc_emc.py @@ -276,6 +276,8 @@ async def test_emc_create_connection_error(default_conf, caplog, mocker): async def test_emc_receive_messages_valid(default_conf, caplog, mocker): + caplog.set_level(logging.DEBUG) + default_conf.update({ "external_message_consumer": { "enabled": True, From 00965d8c069eaf82c5d42c19d3b4b9901dae2183 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 28 Sep 2022 20:20:22 +0200 Subject: [PATCH 039/126] Default to assume stored data only contains complete candles closes #7468 --- freqtrade/data/converter.py | 3 +-- freqtrade/data/history/history_utils.py | 2 +- freqtrade/data/history/idatahandler.py | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/freqtrade/data/converter.py b/freqtrade/data/converter.py index 67461973f..98ed15489 100644 --- a/freqtrade/data/converter.py +++ b/freqtrade/data/converter.py @@ -47,8 +47,7 @@ def ohlcv_to_dataframe(ohlcv: list, timeframe: str, pair: str, *, def clean_ohlcv_dataframe(data: DataFrame, timeframe: str, pair: str, *, - fill_missing: bool = True, - drop_incomplete: bool = True) -> DataFrame: + fill_missing: bool, drop_incomplete: bool) -> DataFrame: """ Cleanse a OHLCV dataframe by * Grouping it by date (removes duplicate tics) diff --git a/freqtrade/data/history/history_utils.py b/freqtrade/data/history/history_utils.py index 6a6e29429..93534e919 100644 --- a/freqtrade/data/history/history_utils.py +++ b/freqtrade/data/history/history_utils.py @@ -26,7 +26,7 @@ def load_pair_history(pair: str, datadir: Path, *, timerange: Optional[TimeRange] = None, fill_up_missing: bool = True, - drop_incomplete: bool = True, + drop_incomplete: bool = False, startup_candles: int = 0, data_format: str = None, data_handler: IDataHandler = None, diff --git a/freqtrade/data/history/idatahandler.py b/freqtrade/data/history/idatahandler.py index c2d92fc4f..80e29f4c0 100644 --- a/freqtrade/data/history/idatahandler.py +++ b/freqtrade/data/history/idatahandler.py @@ -275,7 +275,7 @@ class IDataHandler(ABC): candle_type: CandleType, *, timerange: Optional[TimeRange] = None, fill_missing: bool = True, - drop_incomplete: bool = True, + drop_incomplete: bool = False, startup_candles: int = 0, warn_no_data: bool = True, ) -> DataFrame: From b4fb28e4ef992f9ec44f2814f83dc5fbf7eb10de Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 28 Sep 2022 20:23:56 +0200 Subject: [PATCH 040/126] Update tests for new dataload strategy --- tests/data/test_datahandler.py | 4 ++-- tests/data/test_entryexitanalysis.py | 4 ++-- tests/data/test_history.py | 8 +++---- tests/optimize/test_backtesting.py | 32 ++++++++++++++-------------- 4 files changed, 24 insertions(+), 24 deletions(-) diff --git a/tests/data/test_datahandler.py b/tests/data/test_datahandler.py index 8e1b0050a..5d6d60f84 100644 --- a/tests/data/test_datahandler.py +++ b/tests/data/test_datahandler.py @@ -139,10 +139,10 @@ def test_jsondatahandler_ohlcv_purge(mocker, testdatadir): def test_jsondatahandler_ohlcv_load(testdatadir, caplog): dh = JsonDataHandler(testdatadir) df = dh.ohlcv_load('XRP/ETH', '5m', 'spot') - assert len(df) == 711 + assert len(df) == 712 df_mark = dh.ohlcv_load('UNITTEST/USDT', '1h', candle_type="mark") - assert len(df_mark) == 99 + assert len(df_mark) == 100 df_no_mark = dh.ohlcv_load('UNITTEST/USDT', '1h', 'spot') assert len(df_no_mark) == 0 diff --git a/tests/data/test_entryexitanalysis.py b/tests/data/test_entryexitanalysis.py index 09fbe9957..588220465 100755 --- a/tests/data/test_entryexitanalysis.py +++ b/tests/data/test_entryexitanalysis.py @@ -124,8 +124,8 @@ def test_backtest_analysis_nomock(default_conf, mocker, caplog, testdatadir, tmp assert '0' in captured.out assert '0.01616' in captured.out assert '34.049' in captured.out - assert '0.104104' in captured.out - assert '47.0996' in captured.out + assert '0.104411' in captured.out + assert '52.8292' in captured.out # test group 1 args = get_args(base_args + ['--analysis-groups', "1"]) diff --git a/tests/data/test_history.py b/tests/data/test_history.py index 5642442b2..e7e3d4063 100644 --- a/tests/data/test_history.py +++ b/tests/data/test_history.py @@ -377,8 +377,8 @@ def test_load_partial_missing(testdatadir, caplog) -> None: td = ((end - start).total_seconds() // 60 // 5) + 1 assert td != len(data['UNITTEST/BTC']) - # Shift endtime with +5 - as last candle is dropped (partial candle) - end_real = arrow.get(data['UNITTEST/BTC'].iloc[-1, 0]).shift(minutes=5) + # Shift endtime with +5 + end_real = arrow.get(data['UNITTEST/BTC'].iloc[-1, 0]) assert log_has(f'UNITTEST/BTC, spot, 5m, ' f'data ends at {end_real.strftime(DATETIME_PRINT_FORMAT)}', caplog) @@ -447,7 +447,7 @@ def test_get_timerange(default_conf, mocker, testdatadir) -> None: ) min_date, max_date = get_timerange(data) assert min_date.isoformat() == '2017-11-04T23:02:00+00:00' - assert max_date.isoformat() == '2017-11-14T22:58:00+00:00' + assert max_date.isoformat() == '2017-11-14T22:59:00+00:00' def test_validate_backtest_data_warn(default_conf, mocker, caplog, testdatadir) -> None: @@ -470,7 +470,7 @@ def test_validate_backtest_data_warn(default_conf, mocker, caplog, testdatadir) min_date, max_date, timeframe_to_minutes('1m')) assert len(caplog.record_tuples) == 1 assert log_has( - "UNITTEST/BTC has missing frames: expected 14396, got 13680, that's 716 missing values", + "UNITTEST/BTC has missing frames: expected 14397, got 13681, that's 716 missing values", caplog) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index bd87b2b42..907e97fb7 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -80,7 +80,7 @@ def load_data_test(what, testdatadir): data.loc[:, 'close'] = np.sin(data.index * hz) / 1000 + base return {'UNITTEST/BTC': clean_ohlcv_dataframe(data, timeframe='1m', pair='UNITTEST/BTC', - fill_missing=True)} + fill_missing=True, drop_incomplete=True)} # FIX: fixturize this? @@ -323,7 +323,7 @@ def test_data_to_dataframe_bt(default_conf, mocker, testdatadir) -> None: backtesting = Backtesting(default_conf) backtesting._set_strategy(backtesting.strategylist[0]) processed = backtesting.strategy.advise_all_indicators(data) - assert len(processed['UNITTEST/BTC']) == 102 + assert len(processed['UNITTEST/BTC']) == 103 # Load strategy to compare the result between Backtesting function and strategy are the same strategy = StrategyResolver.load_strategy(default_conf) @@ -1165,9 +1165,9 @@ def test_backtest_start_timerange(default_conf, mocker, caplog, testdatadir): 'Parameter --timerange detected: 1510694220-1510700340 ...', f'Using data directory: {testdatadir} ...', 'Loading data from 2017-11-14 20:57:00 ' - 'up to 2017-11-14 22:58:00 (0 days).', + 'up to 2017-11-14 22:59:00 (0 days).', 'Backtesting with data from 2017-11-14 21:17:00 ' - 'up to 2017-11-14 22:58:00 (0 days).', + 'up to 2017-11-14 22:59:00 (0 days).', 'Parameter --enable-position-stacking detected ...' ] @@ -1244,9 +1244,9 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir): 'Parameter --timerange detected: 1510694220-1510700340 ...', f'Using data directory: {testdatadir} ...', 'Loading data from 2017-11-14 20:57:00 ' - 'up to 2017-11-14 22:58:00 (0 days).', + 'up to 2017-11-14 22:59:00 (0 days).', 'Backtesting with data from 2017-11-14 21:17:00 ' - 'up to 2017-11-14 22:58:00 (0 days).', + 'up to 2017-11-14 22:59:00 (0 days).', 'Parameter --enable-position-stacking detected ...', f'Running backtesting for Strategy {CURRENT_TEST_STRATEGY}', 'Running backtesting for Strategy StrategyTestV2', @@ -1355,9 +1355,9 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat 'Parameter --timerange detected: 1510694220-1510700340 ...', f'Using data directory: {testdatadir} ...', 'Loading data from 2017-11-14 20:57:00 ' - 'up to 2017-11-14 22:58:00 (0 days).', + 'up to 2017-11-14 22:59:00 (0 days).', 'Backtesting with data from 2017-11-14 21:17:00 ' - 'up to 2017-11-14 22:58:00 (0 days).', + 'up to 2017-11-14 22:59:00 (0 days).', 'Parameter --enable-position-stacking detected ...', f'Running backtesting for Strategy {CURRENT_TEST_STRATEGY}', 'Running backtesting for Strategy StrategyTestV2', @@ -1371,7 +1371,7 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat assert 'EXIT REASON STATS' in captured.out assert 'DAY BREAKDOWN' in captured.out assert 'LEFT OPEN TRADES REPORT' in captured.out - assert '2017-11-14 21:17:00 -> 2017-11-14 22:58:00 | Max open trades : 1' in captured.out + assert '2017-11-14 21:17:00 -> 2017-11-14 22:59:00 | Max open trades : 1' in captured.out assert 'STRATEGY SUMMARY' in captured.out @@ -1503,9 +1503,9 @@ def test_backtest_start_nomock_futures(default_conf_usdt, mocker, 'Parameter -i/--timeframe detected ... Using timeframe: 1h ...', f'Using data directory: {testdatadir} ...', 'Loading data from 2021-11-17 01:00:00 ' - 'up to 2021-11-21 03:00:00 (4 days).', + 'up to 2021-11-21 04:00:00 (4 days).', 'Backtesting with data from 2021-11-17 21:00:00 ' - 'up to 2021-11-21 03:00:00 (3 days).', + 'up to 2021-11-21 04:00:00 (3 days).', 'XRP/USDT, funding_rate, 8h, data starts at 2021-11-18 00:00:00', 'XRP/USDT, mark, 8h, data starts at 2021-11-18 00:00:00', f'Running backtesting for Strategy {CURRENT_TEST_STRATEGY}', @@ -1616,9 +1616,9 @@ def test_backtest_start_multi_strat_nomock_detail(default_conf, mocker, 'Parameter --timeframe-detail detected, using 1m for intra-candle backtesting ...', f'Using data directory: {testdatadir} ...', 'Loading data from 2019-10-11 00:00:00 ' - 'up to 2019-10-13 11:10:00 (2 days).', + 'up to 2019-10-13 11:15:00 (2 days).', 'Backtesting with data from 2019-10-11 01:40:00 ' - 'up to 2019-10-13 11:10:00 (2 days).', + 'up to 2019-10-13 11:15:00 (2 days).', f'Running backtesting for Strategy {CURRENT_TEST_STRATEGY}', ] @@ -1719,7 +1719,7 @@ def test_backtest_start_multi_strat_caching(default_conf, mocker, caplog, testda 'Parameter --timerange detected: 1510694220-1510700340 ...', f'Using data directory: {testdatadir} ...', 'Loading data from 2017-11-14 20:57:00 ' - 'up to 2017-11-14 22:58:00 (0 days).', + 'up to 2017-11-14 22:59:00 (0 days).', 'Parameter --enable-position-stacking detected ...', ] @@ -1732,7 +1732,7 @@ def test_backtest_start_multi_strat_caching(default_conf, mocker, caplog, testda 'Running backtesting for Strategy StrategyTestV2', 'Running backtesting for Strategy StrategyTestV3', 'Ignoring max_open_trades (--disable-max-market-positions was used) ...', - 'Backtesting with data from 2017-11-14 21:17:00 up to 2017-11-14 22:58:00 (0 days).', + 'Backtesting with data from 2017-11-14 21:17:00 up to 2017-11-14 22:59:00 (0 days).', ] elif run_id == '2' and min_backtest_date < start_time: assert backtestmock.call_count == 0 @@ -1745,7 +1745,7 @@ def test_backtest_start_multi_strat_caching(default_conf, mocker, caplog, testda 'Reusing result of previous backtest for StrategyTestV2', 'Running backtesting for Strategy StrategyTestV3', 'Ignoring max_open_trades (--disable-max-market-positions was used) ...', - 'Backtesting with data from 2017-11-14 21:17:00 up to 2017-11-14 22:58:00 (0 days).', + 'Backtesting with data from 2017-11-14 21:17:00 up to 2017-11-14 22:59:00 (0 days).', ] assert backtestmock.call_count == 1 From 38aca8e908fb532f44e889d35257961437866b93 Mon Sep 17 00:00:00 2001 From: robcaulk Date: Fri, 30 Sep 2022 00:22:31 +0200 Subject: [PATCH 041/126] fix failing svm test --- tests/freqai/test_freqai_datakitchen.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/freqai/test_freqai_datakitchen.py b/tests/freqai/test_freqai_datakitchen.py index f7446420d..b99ac236d 100644 --- a/tests/freqai/test_freqai_datakitchen.py +++ b/tests/freqai/test_freqai_datakitchen.py @@ -86,7 +86,7 @@ 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.09%", + "SVM detected 8.66%", caplog, ) From be48131185764d0b707e706de4f0aa6d2a688193 Mon Sep 17 00:00:00 2001 From: robcaulk Date: Fri, 30 Sep 2022 00:33:08 +0200 Subject: [PATCH 042/126] make shuffle false in constants --- freqtrade/constants.py | 1 + freqtrade/freqai/data_kitchen.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index e14e81343..acab8489c 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -567,6 +567,7 @@ CONF_SCHEMA = { "properties": { "test_size": {"type": "number"}, "random_state": {"type": "integer"}, + "shuffle": {"type": "boolean", "default": False} }, }, "model_training_parameters": { diff --git a/freqtrade/freqai/data_kitchen.py b/freqtrade/freqai/data_kitchen.py index 9e22667f3..5cf9b2f03 100644 --- a/freqtrade/freqai/data_kitchen.py +++ b/freqtrade/freqai/data_kitchen.py @@ -134,7 +134,8 @@ class FreqaiDataKitchen: """ feat_dict = self.freqai_config["feature_parameters"] - shuffle = self.freqai_config.get('data_split_parameters', {}).get('shuffle', False) + if 'shuffle' not in self.freqai_config['data_split_parameters']: + self.freqai_config["data_split_parameters"].update({'shuffle': False}) weights: npt.ArrayLike if feat_dict.get("weight_factor", 0) > 0: @@ -154,7 +155,6 @@ class FreqaiDataKitchen: filtered_dataframe[: filtered_dataframe.shape[0]], labels, weights, - shuffle=shuffle, **self.config["freqai"]["data_split_parameters"], ) else: From 34951f59d2c54aaf6bcb02d8bf6810f544dca877 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 30 Sep 2022 06:44:19 +0200 Subject: [PATCH 043/126] Update failing tests --- tests/data/test_btanalysis.py | 2 +- tests/strategy/test_interface.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index dab76d0cb..ec7b457ea 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -235,7 +235,7 @@ def test_calculate_market_change(testdatadir): data = load_data(datadir=testdatadir, pairs=pairs, timeframe='5m') result = calculate_market_change(data) assert isinstance(result, float) - assert pytest.approx(result) == 0.00955514 + assert pytest.approx(result) == 0.01100002 def test_combine_dataframes_with_mean(testdatadir): diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 070e78b1d..294021c83 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -288,7 +288,7 @@ def test_advise_all_indicators(default_conf, testdatadir) -> None: data = load_data(testdatadir, '1m', ['UNITTEST/BTC'], timerange=timerange, fill_up_missing=True) processed = strategy.advise_all_indicators(data) - assert len(processed['UNITTEST/BTC']) == 102 # partial candle was removed + assert len(processed['UNITTEST/BTC']) == 103 def test_populate_any_indicators(default_conf, testdatadir) -> None: @@ -300,7 +300,7 @@ def test_populate_any_indicators(default_conf, testdatadir) -> None: processed = strategy.populate_any_indicators('UNITTEST/BTC', data, '5m') assert processed == data assert id(processed) == id(data) - assert len(processed['UNITTEST/BTC']) == 102 # partial candle was removed + assert len(processed['UNITTEST/BTC']) == 103 def test_freqai_not_initialized(default_conf) -> None: From 2d2ff2fff6c45c366e58289ea668c71ccc57da53 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 30 Sep 2022 07:02:18 +0200 Subject: [PATCH 044/126] remove unnecessary assignments and comments --- freqtrade/freqtradebot.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index e912fa832..175f6f148 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1393,16 +1393,10 @@ class FreqtradeBot(LoggingMixin): trade.open_order_id = None logger.info(f'{side} Order timeout for {trade}.') else: - # if trade is partially complete, edit the stake details for the trade - # and close the order - # cancel_order may not contain the full order dict, so we need to fallback - # to the order dict acquired before cancelling. - # we need to fall back to the values from order if corder does not contain these keys. + # update_trade_state (and subsequently recalc_trade_from_orders) will handle updates + # to the trade object trade.amount = filled_amount - # * Check edge cases, we don't want to make leverage > 1.0 if we don't have to - # * (for leverage modes which aren't isolated futures) - trade.stake_amount = trade.amount * trade.open_rate / trade.leverage self.update_trade_state(trade, trade.open_order_id, corder) trade.open_order_id = None @@ -1442,8 +1436,6 @@ class FreqtradeBot(LoggingMixin): trade.close_rate_requested = None trade.close_profit = None trade.close_profit_abs = None - trade.close_date = None - trade.is_open = True trade.open_order_id = None trade.exit_reason = None cancelled = True From 561600e98ba85da1992bc69dbccda844d77580ca Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 30 Sep 2022 07:05:21 +0200 Subject: [PATCH 045/126] Remove false test statements a trade is ONLY closed on `.close()` - which will only happen once the last order has been filled. --- tests/test_freqtradebot.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 5fe4d4011..02a3b7cf6 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -2673,7 +2673,6 @@ def test_manage_open_orders_exit_usercustom( open_trade_usdt.open_date = arrow.utcnow().shift(hours=-5).datetime open_trade_usdt.close_date = arrow.utcnow().shift(minutes=-601).datetime open_trade_usdt.close_profit_abs = 0.001 - open_trade_usdt.is_open = False Trade.query.session.add(open_trade_usdt) Trade.commit() @@ -2687,7 +2686,6 @@ def test_manage_open_orders_exit_usercustom( freqtrade.manage_open_orders() assert cancel_order_mock.call_count == 0 assert rpc_mock.call_count == 1 - assert open_trade_usdt.is_open is False assert freqtrade.strategy.check_exit_timeout.call_count == 1 assert freqtrade.strategy.check_entry_timeout.call_count == 0 @@ -2697,7 +2695,6 @@ def test_manage_open_orders_exit_usercustom( freqtrade.manage_open_orders() assert cancel_order_mock.call_count == 0 assert rpc_mock.call_count == 1 - assert open_trade_usdt.is_open is False assert freqtrade.strategy.check_exit_timeout.call_count == 1 assert freqtrade.strategy.check_entry_timeout.call_count == 0 @@ -2707,7 +2704,6 @@ def test_manage_open_orders_exit_usercustom( freqtrade.manage_open_orders() assert cancel_order_mock.call_count == 1 assert rpc_mock.call_count == 2 - assert open_trade_usdt.is_open is True assert freqtrade.strategy.check_exit_timeout.call_count == 1 assert freqtrade.strategy.check_entry_timeout.call_count == 0 @@ -2755,7 +2751,6 @@ def test_manage_open_orders_exit( open_trade_usdt.open_date = arrow.utcnow().shift(hours=-5).datetime open_trade_usdt.close_date = arrow.utcnow().shift(minutes=-601).datetime open_trade_usdt.close_profit_abs = 0.001 - open_trade_usdt.is_open = False open_trade_usdt.is_short = is_short Trade.query.session.add(open_trade_usdt) @@ -2796,7 +2791,6 @@ def test_check_handle_cancelled_exit( open_trade_usdt.open_date = arrow.utcnow().shift(hours=-5).datetime open_trade_usdt.close_date = arrow.utcnow().shift(minutes=-601).datetime - open_trade_usdt.is_open = False open_trade_usdt.is_short = is_short Trade.query.session.add(open_trade_usdt) From 7dd984e25e5472f0a2fa069b81bc6f520e3ac1cd Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 30 Sep 2022 07:13:57 +0200 Subject: [PATCH 046/126] Simplify cancel_entry --- freqtrade/freqtradebot.py | 7 ++----- tests/test_freqtradebot.py | 2 ++ 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 175f6f148..83089152a 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1343,11 +1343,12 @@ class FreqtradeBot(LoggingMixin): replacing: Optional[bool] = False ) -> bool: """ - Buy cancel - cancel order + entry cancel - cancel order :param replacing: Replacing order - prevent trade deletion. :return: True if order was fully cancelled """ was_trade_fully_canceled = False + side = trade.entry_side.capitalize() # Cancelled orders may have the status of 'canceled' or 'closed' if order['status'] not in constants.NON_OPEN_EXCHANGE_STATES: @@ -1374,7 +1375,6 @@ class FreqtradeBot(LoggingMixin): corder = order reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE'] - side = trade.entry_side.capitalize() logger.info('%s order %s for %s.', side, reason, trade) # Using filled to determine the filled amount @@ -1388,15 +1388,12 @@ class FreqtradeBot(LoggingMixin): was_trade_fully_canceled = True reason += f", {constants.CANCEL_REASON['FULLY_CANCELLED']}" else: - # FIXME TODO: This could possibly reworked to not duplicate the code 15 lines below. self.update_trade_state(trade, trade.open_order_id, corder) trade.open_order_id = None logger.info(f'{side} Order timeout for {trade}.') else: # update_trade_state (and subsequently recalc_trade_from_orders) will handle updates # to the trade object - trade.amount = filled_amount - self.update_trade_state(trade, trade.open_order_id, corder) trade.open_order_id = None diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 02a3b7cf6..7c7132bdd 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -2998,6 +2998,7 @@ def test_handle_cancel_enter(mocker, caplog, default_conf_usdt, limit_order, is_ trade.open_rate = 200 trade.is_short = False trade.entry_side = "buy" + trade.amount = 100 l_order['filled'] = 0.0 l_order['status'] = 'open' trade.nr_of_successful_entries = 0 @@ -3086,6 +3087,7 @@ def test_handle_cancel_enter_corder_empty(mocker, default_conf_usdt, limit_order trade.entry_side = "buy" trade.open_order_id = "open_order_noop" trade.nr_of_successful_entries = 0 + trade.amount = 100 l_order['filled'] = 0.0 l_order['status'] = 'open' reason = CANCEL_REASON['TIMEOUT'] From f6a0d677d2050dba3e805c4a69c5cbb65941e695 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 30 Sep 2022 07:23:50 +0200 Subject: [PATCH 047/126] Remove pointless notification assignment --- freqtrade/freqtradebot.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 83089152a..532d5d3d8 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1692,11 +1692,6 @@ class FreqtradeBot(LoggingMixin): 'stake_amount': trade.stake_amount, } - if 'fiat_display_currency' in self.config: - msg.update({ - 'fiat_currency': self.config['fiat_display_currency'], - }) - # Send the message self.rpc.send_msg(msg) From 0d8dfc1a922a8fb6550f94a449f1c56c7e63ef5b Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 30 Sep 2022 13:47:26 +0200 Subject: [PATCH 048/126] Force joblib update via setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 0581081fa..d3f9ea7c0 100644 --- a/setup.py +++ b/setup.py @@ -72,7 +72,7 @@ setup( 'pandas', 'tables', 'blosc', - 'joblib', + 'joblib>=1.2.0', 'pyarrow; platform_machine != "armv7l"', 'fastapi', 'uvicorn', From cc06c60fd8f8d45cc9b4643d005dcef209351d57 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 30 Sep 2022 15:43:05 +0200 Subject: [PATCH 049/126] Fix pandas deprecation warnings from freqAI --- freqtrade/freqai/data_kitchen.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/freqai/data_kitchen.py b/freqtrade/freqai/data_kitchen.py index f4fa4e5fd..400e70fc8 100644 --- a/freqtrade/freqai/data_kitchen.py +++ b/freqtrade/freqai/data_kitchen.py @@ -210,7 +210,7 @@ class FreqaiDataKitchen: 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_df).any(1) # get the rows that have NaNs, + drop_index = pd.isnull(filtered_df).any(axis=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) @@ -221,7 +221,7 @@ class FreqaiDataKitchen: # about removing any row with NaNs # if labels has multiple columns (user wants to train multiple modelEs), we detect here labels = unfiltered_df.filter(label_list, axis=1) - drop_index_labels = pd.isnull(labels).any(1) + drop_index_labels = pd.isnull(labels).any(axis=1) drop_index_labels = drop_index_labels.replace(True, 1).replace(False, 0) dates = unfiltered_df['date'] filtered_df = filtered_df[ @@ -249,7 +249,7 @@ 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_df).any(1) + drop_index = pd.isnull(filtered_df).any(axis=1) self.data["filter_drop_index_prediction"] = drop_index filtered_df.fillna(0, inplace=True) # replacing all NaNs with zeros to avoid issues in 'prediction', but any prediction @@ -808,7 +808,7 @@ class FreqaiDataKitchen: :, :no_prev_pts ] distances = distances.replace([np.inf, -np.inf], np.nan) - drop_index = pd.isnull(distances).any(1) + drop_index = pd.isnull(distances).any(axis=1) distances = distances[drop_index == 0] inliers = pd.DataFrame(index=distances.index) From bd664580fbd3459884d61c6db46cff37e366bfb9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 30 Sep 2022 15:07:18 +0200 Subject: [PATCH 050/126] Don't unnecessarily reset order_id --- freqtrade/freqtradebot.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 532d5d3d8..387bae534 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1345,7 +1345,7 @@ class FreqtradeBot(LoggingMixin): """ entry cancel - cancel order :param replacing: Replacing order - prevent trade deletion. - :return: True if order was fully cancelled + :return: True if trade was fully cancelled """ was_trade_fully_canceled = False side = trade.entry_side.capitalize() @@ -1389,14 +1389,12 @@ class FreqtradeBot(LoggingMixin): reason += f", {constants.CANCEL_REASON['FULLY_CANCELLED']}" else: self.update_trade_state(trade, trade.open_order_id, corder) - trade.open_order_id = None logger.info(f'{side} Order timeout for {trade}.') else: # update_trade_state (and subsequently recalc_trade_from_orders) will handle updates # to the trade object self.update_trade_state(trade, trade.open_order_id, corder) - trade.open_order_id = None logger.info(f'Partial {trade.entry_side} order timeout for {trade}.') reason += f", {constants.CANCEL_REASON['PARTIALLY_FILLED']}" From d462f4029998a8ff22d98aa7763851248baecb2d Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 30 Sep 2022 15:26:34 +0200 Subject: [PATCH 051/126] Simple test improvements --- tests/test_freqtradebot.py | 40 +++++++++++++++++++++++++++++--------- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 7c7132bdd..0f1a05ab4 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -2661,6 +2661,7 @@ def test_manage_open_orders_exit_usercustom( rpc_mock = patch_RPCManager(mocker) cancel_order_mock = MagicMock() patch_exchange(mocker) + mocker.patch('freqtrade.exchange.Exchange.get_min_pair_stake_amount', return_value=0.0) et_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.execute_trade_exit') mocker.patch.multiple( 'freqtrade.exchange.Exchange', @@ -2744,7 +2745,8 @@ def test_manage_open_orders_exit( 'freqtrade.exchange.Exchange', fetch_ticker=ticker_usdt, fetch_order=MagicMock(return_value=limit_sell_order_old), - cancel_order=cancel_order_mock + cancel_order=cancel_order_mock, + get_min_pair_stake_amount=MagicMock(return_value=0), ) freqtrade = FreqtradeBot(default_conf_usdt) @@ -3117,20 +3119,21 @@ def test_handle_cancel_exit_limit(mocker, default_conf_usdt, fee) -> None: amount=2, exchange='binance', open_rate=0.245441, - open_order_id="123456", + open_order_id="sell_123456", open_date=arrow.utcnow().shift(days=-2).datetime, fee_open=fee.return_value, fee_close=fee.return_value, close_rate=0.555, close_date=arrow.utcnow().datetime, exit_reason="sell_reason_whatever", + stake_amount=0.245441 * 2, ) trade.orders = [ - Order( + Order( ft_order_side='buy', ft_pair=trade.pair, - ft_is_open=True, - order_id='123456', + ft_is_open=False, + order_id='buy_123456', status="closed", symbol=trade.pair, order_type="market", @@ -3143,15 +3146,33 @@ def test_handle_cancel_exit_limit(mocker, default_conf_usdt, fee) -> None: order_date=trade.open_date, order_filled_date=trade.open_date, ), + Order( + ft_order_side='sell', + ft_pair=trade.pair, + ft_is_open=True, + order_id='sell_123456', + status="open", + symbol=trade.pair, + order_type="limit", + side="sell", + price=trade.open_rate, + average=trade.open_rate, + filled=0.0, + remaining=trade.amount, + cost=trade.open_rate * trade.amount, + order_date=trade.open_date, + order_filled_date=trade.open_date, + ), ] - order = {'id': "123456", + order = {'id': "sell_123456", 'remaining': 1, 'amount': 1, 'status': "open"} reason = CANCEL_REASON['TIMEOUT'] + send_msg_mock.reset_mock() assert freqtrade.handle_cancel_exit(trade, order, reason) assert cancel_order_mock.call_count == 1 - assert send_msg_mock.call_count == 2 + assert send_msg_mock.call_count == 1 assert trade.close_rate is None assert trade.exit_reason is None @@ -3177,8 +3198,9 @@ def test_handle_cancel_exit_limit(mocker, default_conf_usdt, fee) -> None: def test_handle_cancel_exit_cancel_exception(mocker, default_conf_usdt) -> None: patch_RPCManager(mocker) patch_exchange(mocker) - mocker.patch( - 'freqtrade.exchange.Exchange.cancel_order_with_result', side_effect=InvalidOrderException()) + mocker.patch('freqtrade.exchange.Exchange.get_min_pair_stake_amount', return_value=0.0) + mocker.patch('freqtrade.exchange.Exchange.cancel_order_with_result', + side_effect=InvalidOrderException()) freqtrade = FreqtradeBot(default_conf_usdt) From 649879192b6453520ffa550ba7eb509696989c8b Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 30 Sep 2022 15:12:57 +0200 Subject: [PATCH 052/126] Implement partial sell --- freqtrade/freqtradebot.py | 52 +++++++++++++++++++++------------------ 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 387bae534..67d734ce2 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1409,37 +1409,41 @@ class FreqtradeBot(LoggingMixin): :return: True if exit order was cancelled, false otherwise """ cancelled = False - # if trade is not partially completed, just cancel the order - if order['remaining'] == order['amount'] or order.get('filled') == 0.0: - if not self.exchange.check_order_canceled_empty(order): - try: - # if trade is not partially completed, just delete the order - co = self.exchange.cancel_order_with_result(trade.open_order_id, trade.pair, - trade.amount) - trade.update_order(co) - except InvalidOrderException: - logger.exception( - f"Could not cancel {trade.exit_side} order {trade.open_order_id}") - return False - logger.info('%s order %s for %s.', trade.exit_side.capitalize(), reason, trade) - else: - reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE'] - logger.info('%s order %s for %s.', trade.exit_side.capitalize(), reason, trade) - trade.update_order(order) + # Cancelled orders may have the status of 'canceled' or 'closed' + if order['status'] not in constants.NON_OPEN_EXCHANGE_STATES: + filled_val: float = order.get('filled', 0.0) or 0.0 + filled_rem_stake = trade.stake_amount - filled_val * trade.open_rate + minstake = self.exchange.get_min_pair_stake_amount( + trade.pair, trade.open_rate, self.strategy.stoploss) + # Double-check remaining amount + if filled_val > 0 and minstake and filled_rem_stake < minstake: + logger.warning( + f"Order {trade.open_order_id} for {trade.pair} not cancelled, " + f"as the filled amount of {filled_val} would result in an unexitable trade.") + return False + try: + co = self.exchange.cancel_order_with_result(trade.open_order_id, trade.pair, + trade.amount) + except InvalidOrderException: + logger.exception( + f"Could not cancel {trade.exit_side} order {trade.open_order_id}") + return False trade.close_rate = None trade.close_rate_requested = None trade.close_profit = None trade.close_profit_abs = None - trade.open_order_id = None trade.exit_reason = None - cancelled = True - self.wallets.update() - else: - # TODO: figure out how to handle partially complete sell orders - reason = constants.CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN'] - cancelled = False + self.update_trade_state(trade, trade.open_order_id, co) + logger.info(f'{trade.exit_side.capitalize()} order {reason} for {trade}.') + cancelled = True + else: + reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE'] + logger.info(f'{trade.exit_side.capitalize()} order {reason} for {trade}.') + self.update_trade_state(trade, trade.open_order_id, order) + + self.wallets.update() order_obj = trade.select_order_by_order_id(order['id']) if not order_obj: raise DependencyException( From c946d30596c46c823ad02a738c072594e555f24d Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 30 Sep 2022 16:17:48 +0200 Subject: [PATCH 053/126] Add partial cancel message --- freqtrade/freqtradebot.py | 23 ++++++++++++++++++----- tests/test_freqtradebot.py | 16 +++++++++++++++- 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 67d734ce2..b98135fa5 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1416,11 +1416,24 @@ class FreqtradeBot(LoggingMixin): minstake = self.exchange.get_min_pair_stake_amount( trade.pair, trade.open_rate, self.strategy.stoploss) # Double-check remaining amount - if filled_val > 0 and minstake and filled_rem_stake < minstake: - logger.warning( - f"Order {trade.open_order_id} for {trade.pair} not cancelled, " - f"as the filled amount of {filled_val} would result in an unexitable trade.") - return False + if filled_val > 0: + reason = constants.CANCEL_REASON['PARTIALLY_FILLED'] + if minstake and filled_rem_stake < minstake: + logger.warning( + f"Order {trade.open_order_id} for {trade.pair} not cancelled, " + f"as the filled amount of {filled_val} would result in an unexitable trade.") + reason = constants.CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN'] + + order_obj = trade.select_order_by_order_id(order['id']) + if not order_obj: + raise DependencyException( + f"Order_obj not found for {order['id']}. This should not have happened.") + self._notify_exit_cancel( + trade, + order_type=self.strategy.order_types['exit'], + reason=reason, order=order_obj, sub_trade=trade.amount != order['amount'] + ) + return False try: co = self.exchange.cancel_order_with_result(trade.open_order_id, trade.pair, diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 0f1a05ab4..415abbc10 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -3111,6 +3111,9 @@ def test_handle_cancel_exit_limit(mocker, default_conf_usdt, fee) -> None: cancel_order=cancel_order_mock, ) mocker.patch('freqtrade.exchange.Exchange.get_rate', return_value=0.245441) + mocker.patch('freqtrade.exchange.Exchange.get_min_pair_stake_amount', return_value=0.2) + + mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_order_fee') freqtrade = FreqtradeBot(default_conf_usdt) @@ -3178,7 +3181,9 @@ def test_handle_cancel_exit_limit(mocker, default_conf_usdt, fee) -> None: send_msg_mock.reset_mock() + # Partial exit - below exit threshold order['amount'] = 2 + order['filled'] = 1.9 assert not freqtrade.handle_cancel_exit(trade, order, reason) # Assert cancel_order was not called (callcount remains unchanged) assert cancel_order_mock.call_count == 1 @@ -3188,12 +3193,21 @@ def test_handle_cancel_exit_limit(mocker, default_conf_usdt, fee) -> None: assert not freqtrade.handle_cancel_exit(trade, order, reason) - send_msg_mock.call_args_list[0][0][0]['reason'] = CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN'] + assert (send_msg_mock.call_args_list[0][0][0]['reason'] + == CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN']) # Message should not be iterated again assert trade.exit_order_status == CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN'] assert send_msg_mock.call_count == 1 + send_msg_mock.reset_mock() + + order['filled'] = 1 + assert freqtrade.handle_cancel_exit(trade, order, reason) + assert send_msg_mock.call_count == 1 + assert (send_msg_mock.call_args_list[0][0][0]['reason'] + == CANCEL_REASON['PARTIALLY_FILLED']) + def test_handle_cancel_exit_cancel_exception(mocker, default_conf_usdt) -> None: patch_RPCManager(mocker) From 819488c906a01cc25995d715c3da36656748518e Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 30 Sep 2022 16:59:23 +0200 Subject: [PATCH 054/126] Improve exit message wording --- freqtrade/freqtradebot.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index b98135fa5..37bf032fa 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1446,8 +1446,10 @@ class FreqtradeBot(LoggingMixin): trade.close_rate_requested = None trade.close_profit = None trade.close_profit_abs = None - trade.exit_reason = None + # Set exit_reason for fill message + trade.exit_reason = trade.exit_reason + f", {reason}" if trade.exit_reason else reason self.update_trade_state(trade, trade.open_order_id, co) + trade.exit_reason = None logger.info(f'{trade.exit_side.capitalize()} order {reason} for {trade}.') cancelled = True From 47ef99f5886ca1373256b19ccf1878a9abf4f9bc Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 30 Sep 2022 17:18:27 +0200 Subject: [PATCH 055/126] Simplify interface to notify_exit_cancel --- freqtrade/freqtradebot.py | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 37bf032fa..2b20e40fd 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1420,18 +1420,15 @@ class FreqtradeBot(LoggingMixin): reason = constants.CANCEL_REASON['PARTIALLY_FILLED'] if minstake and filled_rem_stake < minstake: logger.warning( - f"Order {trade.open_order_id} for {trade.pair} not cancelled, " - f"as the filled amount of {filled_val} would result in an unexitable trade.") + f"Order {trade.open_order_id} for {trade.pair} not cancelled, as " + f"the filled amount of {filled_val} would result in an unexitable trade.") reason = constants.CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN'] - order_obj = trade.select_order_by_order_id(order['id']) - if not order_obj: - raise DependencyException( - f"Order_obj not found for {order['id']}. This should not have happened.") self._notify_exit_cancel( trade, order_type=self.strategy.order_types['exit'], - reason=reason, order=order_obj, sub_trade=trade.amount != order['amount'] + reason=reason, order_id=order['id'], + sub_trade=trade.amount != order['amount'] ) return False @@ -1459,16 +1456,11 @@ class FreqtradeBot(LoggingMixin): self.update_trade_state(trade, trade.open_order_id, order) self.wallets.update() - order_obj = trade.select_order_by_order_id(order['id']) - if not order_obj: - raise DependencyException( - f"Order_obj not found for {order['id']}. This should not have happened.") - sub_trade = order_obj.amount != trade.amount self._notify_exit_cancel( trade, order_type=self.strategy.order_types['exit'], - reason=reason, order=order_obj, sub_trade=sub_trade + reason=reason, order_id=order['id'], sub_trade=trade.amount != order['amount'] ) return cancelled @@ -1665,7 +1657,7 @@ class FreqtradeBot(LoggingMixin): self.rpc.send_msg(msg) def _notify_exit_cancel(self, trade: Trade, order_type: str, reason: str, - order: Order, sub_trade: bool = False) -> None: + order_id: str, sub_trade: bool = False) -> None: """ Sends rpc notification when a sell cancel occurred. """ @@ -1674,6 +1666,11 @@ class FreqtradeBot(LoggingMixin): else: trade.exit_order_status = reason + order = trade.select_order_by_order_id(order_id) + if not order: + raise DependencyException( + f"Order_obj not found for {order_id}. This should not have happened.") + profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested profit_trade = trade.calc_profit(rate=profit_rate) current_rate = self.exchange.get_rate( From cdc01a07819f2634ce33c0bf9dc666924673e1ad Mon Sep 17 00:00:00 2001 From: Emre Date: Fri, 30 Sep 2022 15:22:05 -0700 Subject: [PATCH 056/126] Fix feature list match for pca --- freqtrade/freqai/data_kitchen.py | 5 +++++ freqtrade/freqai/freqai_interface.py | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/freqtrade/freqai/data_kitchen.py b/freqtrade/freqai/data_kitchen.py index 400e70fc8..c05900bad 100644 --- a/freqtrade/freqai/data_kitchen.py +++ b/freqtrade/freqai/data_kitchen.py @@ -1,5 +1,6 @@ import copy import logging +import re import shutil from datetime import datetime, timezone from math import cos, sin @@ -881,6 +882,10 @@ class FreqaiDataKitchen: """ column_names = dataframe.columns features = [c for c in column_names if "%" in c] + pca_features = [c for c in column_names if re.search(r"^PC\d+$", c)] + if not features and pca_features: + features = pca_features + if not features: raise OperationalException("Could not find any features!") diff --git a/freqtrade/freqai/freqai_interface.py b/freqtrade/freqai/freqai_interface.py index d9f917338..bf625b2a7 100644 --- a/freqtrade/freqai/freqai_interface.py +++ b/freqtrade/freqai/freqai_interface.py @@ -434,6 +434,10 @@ class IFreqaiModel(ABC): feature_list = dk.data["training_features_list_raw"] else: feature_list = dk.data['training_features_list'] + + if self.ft_params.get('principal_component_analysis', False): + feature_list = dk.data['training_features_list'] + if dk.training_features_list != feature_list: raise OperationalException( "Trying to access pretrained model with `identifier` " From fad90269391fb35a92c3fc03fa24d3b51720bc28 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 1 Oct 2022 08:35:51 +0200 Subject: [PATCH 057/126] Update updating docs closes #7507 --- docs/updating.md | 9 +++++++++ docs/windows_installation.md | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/updating.md b/docs/updating.md index 8dc7279a4..893bc846e 100644 --- a/docs/updating.md +++ b/docs/updating.md @@ -37,3 +37,12 @@ pip install -e . # Ensure freqUI is at the latest version freqtrade install-ui ``` + +### Problems updating + +Update-problems usually come missing dependencies (you didn't follow the above instructions) - or from updated dependencies, which fail to install (for example TA-lib). +Please refer to the corresponding installation sections (common problems linked below) + +Common problems and their solutions: + +* [ta-lib update on windows](windows_installation.md#2-install-ta-lib) diff --git a/docs/windows_installation.md b/docs/windows_installation.md index 9fbbf8250..5cfae8c10 100644 --- a/docs/windows_installation.md +++ b/docs/windows_installation.md @@ -34,7 +34,7 @@ python -m venv .env .env\Scripts\activate.ps1 # optionally install ta-lib from wheel # Eventually adjust the below filename to match the downloaded wheel -pip install --find-links build_helpers\ TA-Lib +pip install --find-links build_helpers\ TA-Lib -U pip install -r requirements.txt pip install -e . freqtrade From 545d65235261563dbc4b482dea82e2d76f0c5440 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 1 Oct 2022 09:02:05 +0200 Subject: [PATCH 058/126] Update okx exception wording --- freqtrade/exchange/okx.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/exchange/okx.py b/freqtrade/exchange/okx.py index 2db5fb6a9..fe1c94017 100644 --- a/freqtrade/exchange/okx.py +++ b/freqtrade/exchange/okx.py @@ -78,7 +78,7 @@ class Okx(Exchange): raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( - f'Could not set leverage due to {e.__class__.__name__}. Message: {e}') from e + f'Error in additional_exchange_init due to {e.__class__.__name__}. Message: {e}') from e except ccxt.BaseError as e: raise OperationalException(e) from e From a96aa568bfa81c7c78ba60cc676483f3903a2d74 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 1 Oct 2022 09:21:40 +0200 Subject: [PATCH 059/126] Add binance futures mode checks closes #7505 --- docs/assets/binance_futures_settings.png | Bin 0 -> 81500 bytes docs/exchanges.md | 19 ++++++++++---- freqtrade/exchange/binance.py | 31 +++++++++++++++++++++++ tests/exchange/test_binance.py | 18 +++++++++++++ tests/exchange/test_ccxt_compat.py | 1 + 5 files changed, 64 insertions(+), 5 deletions(-) create mode 100644 docs/assets/binance_futures_settings.png diff --git a/docs/assets/binance_futures_settings.png b/docs/assets/binance_futures_settings.png new file mode 100644 index 0000000000000000000000000000000000000000..a3f7a2c7032d54c80b2206bad0a38cd5c7729d2a GIT binary patch literal 81500 zcma%iWl&s8xb4O@Ft}^5;KAKp1HnCbaCZw5971ppE`tOQF2UX1-QD$0&aL}?zp9y< zsgds9-Jh37J4$N&Jmla-NB1ppX20D#6sgob=l4EQWSUXUDQw4DI}t^3~- zDv<$=2mr_dSqV`!4};?scOO$tU+&klMl-Lu;ux=FjivNfYrU#Yka5Jx4}b!n^&yVu z$1TbZ#ZB@p)mS{N7#$K9yv=fFfUVx|=w|m;$z~nbXeoWdk*LB3?=#+Z;n88sU8{Wc zm6_A5JCxZx=TYpDs9nF^+Q=w#23FL+tjUBy6F-%xrwdU3+Q@69i(^Xy>(iW&FCdTM z!rT%Rt#f1i?_co0HyZuFcOw7)@0y~>bzjsl%9t8%{(> z9ULsbTozN+?_?d`kt~`bQlLSrs$5P-$0ClEiXRAMvR%MxD~J%NaWKl4u5fr!3@CCEq^;g24PwMT%5Uq`w}RikaAhYb5k%n{(B0q5 z0X9^qUpaJIC(7s(CO-hIj33t4eLtc)e{B>|`Fc~v)l41+fY9IyTF~<_EeL>=Vcf^} zeMq%babbNX6M8i_8=g5OjZAOxnsGaE8&HyQS30%{ot8E>cHD%DhsXhkXLLx9D_<-{N@l+UH9E$dWq z*g$dQ#Kj}d>nYk0reov%+yq&))MpcCZ{@piy`bnV9yC*JcwqdX?z(PvzBI$bZ&4Tz z)p*LT87%;gWmirbQI^IeJ?opPY_7icABTAi2IQkn;}qMlEzitVsxV4G=0J^&lv32gr0&!ZC6ET@0Mt>DJb0bE!x~9x?b$;QV{k+fdC_@#XO>3|5fV;08NM z@s`;wXG8%S=vX#7)2l!Q0Ls&>_|q;*^Yo81?)6F50FO%FR(?M4qhC$=tR>&>52Nl; zTf}+EtG3BXrxZR9JFW(|R%(5dbCKDpX{GQ~^T>iOx@NHQ^#if3cgD9V!7HLuLbrs3 z!*_?_d*L1e>)Dz(cFg6YM%DBCJ6at1Qv}@B!zxAGbc9Smn6YSVGM&|p6aZw6`E(n8 z^{BDl8%;{X#0uTY(Y{NyQ#Sg0MrN)!%-MYB-b74Dy-Rc?G$Wi2JD%uzdSpe<>44PU zT=aCD$blkR#BFtRh)xEKTe@=i?Z{Ja4s?H~fh#|`>e=bOTdu!1F7{gv!?e)0V~uk= zh6IIiOg<`isC?Cdg@X@3ak10H=U5&JKK zHJ-UytMR13Zw`I^%>C0$1OGQ`m)vG_jgO<$JV*S8-4D*JhtmRxuF?PM3tQLPKqdumG#v0J4| zi9qK)b?>7eb1P@E$hT?VE5Nt;S^gI0H-?lc(QI5N3n!D0f)+5wiq2PUqa8K0F9n&+ zOD7-3)`0}B`r`xC(RTJugP-M9_3N%*7-Og~14i)G^$I#)N%7MIo!hc7bev_Vf^t*b$L^rG2(OnYJa9>O~N)m2L9BNyATB2+-H%F?pY z#xIW^DW689uR#hpn2rhx zWV_TKSB+$oq8g8Do}IBNXC9}ki_qDwfwKjK+Dv5?I)sIJHwMNG;<2*IPfwu(EO`(C zRX)WA?_bU*$#(EnGy|prnHtI+pG=VeF%%(n-riJu7EQ#e(yCq%sn>&qy$sa30&Lff z5A6G+QKA@)5J4z;MalfVgDoNHrBgvgC3Y4#i@u=~+ANR1g4PB1V4`5Pl0>2LLD1-BpZ7^vDhOgMA@(W*7%AKjVD`HOgPEC$`So zfmy$`X7!4)$#{5EJ2F=5nq6eMnQ0OFq@ZD468Y~mv|IBieycU41g;29$%4RqZgUGo zkGRzg7F6xr8v47SmaFP@x{uO6FK}d?O^3a5o@ILy#4v2!XEr6#uFCLOuDCF;d3@=K z@f>JG!bh~Yk<8nMg?T#3Xf~qs86rxV2n!fz%QhTJH2mn1K0-y)ay2o$M9a%Pi4OE5cO1wa58j_QiiCkzJ%7)zu6ZBY z3{KB4xH(8~hn%Wy#Yj2-zMF}6%mdd?7w^Q0>O)VO5uk8oZjyOzxdH%)k(x{B0EHSV z;4ktE+Aq2Qs6PpN%b!MWGWpYJMjbL;Ee}23c2b)Fk+{i-1b9H~_vmjPB7WR=Q1XU8r-c-U& z!wgJkh6XJSEWoNWQX=9H4Ff238%wl)!|p$zf&ye{ei}_U0a_ZYw1|iS=fqtO;pIF(bwcOp&xXRX|GBW$)vls$YgQF?oQC<>oLyDIntJ zid4h3m?r2=1a~$-kqzB5>(?Vt6spHvPuzsusptN7y-WMUmNrpA<|oypdbBDZce1%N z#A)x~ z{w?BTMKxPz5=#B8s+N@ln^{Yi3E?@8uvr`h&9|FK4>K!a-16!Fg}}33PV8>Zt z(W@nIt0N29bN}Qt{JwfiK2Bv!Z)mkN!NSzby0@;LtgOG}m`jy-EHW1!ppxxMZ)li? zBwe%E7)KkpneD_K1tpJZNz@ub;yw)CXsCdxqw88m+!#v|Gp{ojXlJiHjLrv5Hmjjh zPIg+a-3b3~Iw<@6!m_j}>0M+P+yG2eDlk3qVt_DQOJmzAsY@(^tN>_6O>-e+T;eAc zOjXk*vI63FmmV*ms1%;bJ{27-(ELIA; z!*n-fZ`&9Ed!b{29Sx0_o56*WexG7phe`8UV2^WdsHTf=usJLEQ`%@~c~ z*66a~*zjp~9`VO{^axd8T*9Y?mgW^iGo0_WEch_LxsGk7l`?&LY}+68ZAl$C-F3TQ0zAGqt7w1cOGFJj zH*k@10}2%Ig#KcS6&_~Zl(dmPl4!ZO9lJ2Q+>O}{aD<|a8gcB6HB$zUKNxr_a=mM< z)S}AoX9Fy6yvj#K_B^ytM*megR6qb8Y~DiCDX#&19(ATFOQTL|jY}Pwmu^JEpC-5Z z86MOYO!Uz|V_7$_*w%CJr0HAiTc>M&0e~gLqPsh1LJAylDhZ$GBFPUA-IJIV+e9=$ zTFF-N6$|)h57~jFmNe_lbe4G<$8>=WdY_%uJ7OoM+s;R?Y|oX<>Hlg)7sC7Qz`f;? zQ-X?%{t2amyMhK9L2hiUmp62fVBp|`Jhv_1;pWNJJum@bkuSXhWj zf&g&}l{*O|qStR9brnp&_LW>$$Ql7a0C5^2BhzR z5%Msv*E-lWsM0dEHlK2aulYu`HwgfNbR)N!#5BN<=mld>>&#j2)9W$`Sb6n2(Z658 zx~Si31lu^s^}jxydhLZ%G^x`#!^t8DT5$PCuibufIZP>@u#r5c_cC;8uPCq}|FD?y zB?W~hHH>LQ)hmzJf60rHAo0#+% z2Jl1?VqnRa;i;weWO&ny4(Gjd2nc`|iIL6=x$@?TpGni&PecUzH@>2i>$XZ-%r>|u zzyRb>Smp5lTDwaM`2!R;t3#YqYu`*=)W3R010LAGel@=KPt2+-7A2j68Td*R8f?8! zcsp_V*OM30<{05)+usT27*9#~YF3ZfQ2&7QRwfMy)+=F0OxW(JR8Bpu&h` zJjq@CV~f|4f&wiqp2OGtIpa<)m|bmF_x)Dq@4}{GHg-}P8O=pC6s({t8zA^g3TIbJ zzY|}k9R0nVUU%LH0Tc^Ml=?k0HMOw^ct_$s#|*}SNw&;cqQ1a69Jw?i;;9Dr;}RHv zm+vDVEv?kk^#~LHos)<|7x$)lN#uXSb#G#5%dyzK>ts#FE(6G^5OWdJm4X>E%7heYA;vL#09!Hbu1-40!wxCGe@V(oV_VCi%a-f)XNY6P zkyj%N?|WU)rbldH#DGpcuG4-ofbP4%XE-$8{n=wMo>;ZF*TQ^ORGeGwq+kj4&FW0ht1JXmB?x{M*RIfAHHbTLO_P6;Oxo z&z5@cL3ygz1Fv6DJ$ywxc7;e09AI<$DX%Hu-}TIzPsPZh)@XyofM6Jx_=GkF41ag} zy2niUxBPlYI*!@;w2HW6{-rY#a_Oo)7hvH8n{PdTBxO2dtsRC&xaVforiKn`IfOo5 z^b*+@YWg}E2X#6ed9Rr?EM$2-CbQSDA6o53NW4$R+KMgunXS-mome-LiHOuSq`}Y$ z3qZD*7cJmZdUa;9Nd10`{;*`%e^(#cm|?VI+?IV6h^uXQm^*rVeSLci6(E%-=yl!S z=Ig!Y^-*_T^o_`LZ+_UDZ2aH3Tx<4U~Da*~|YET_=De7Zh#2mVk*Zhu77 zM-2z_h}NbPuivCB|BlgEu4pRG5*q+=UtV6C-A>gwwV=kXQhrQFU=^e?Ac*b%E!pON zzi}hUeE6X`6BPKd7r7VKg0E@T-`nbcZHlPIvyUnYsm~E`nA!MC+!r1=K0kjRpDjdy z8gh7ah)z0h&q3%b^p-ao9 zJxyfany5enwJzSU!X>b2XH#60!o==eQ`vG!NOCcKbS)igpnp=DWL8q?zrH>288%EK zkJo8i##T|0K9i7;P{{R=!@TuiDIoPnr9GpoqE+1rykHWJLRc;xK2{(*zg%&81s=+O z4pUEl?j!D-Id@bbJgcUe&Ffs>&@PM_&3AkYuH<{ZtTenpcy_I;JM$f5iWPkA*0paX zK?~JaS8w;apq7k_tk22(^1wZ_y1H9h>UDTvfjKb!+Ujlh=d{Tp%QtB?Z%X@eda9i& zc-iSnvDy7Zz;6>4(DOdwEKz%;74}ud?m4RP)}+HGBqeCv-JdXJyXnu1Map$psXJ3m zsF+%@sBTAab|eqht1es967yO-C5Qc;YZUdd*ToIb<`DHMlJ8^uKY)4jWTocrbN2I! zvhSt4@I!MpYpMdrDbO)}t+poe_WazEOULha%(LX|X6{Clsf7Lxodq_?)zuYQ16#5) zSf)A{>BFM=f#S*H!cA|wJ-m318vVm;l?e_Gj(JTg*OiArH6?=<0Wl#nOCbyl4en$waa(^D z78>=K>crBQ6)yzr*;3cu+U_REpn!lB#X=b>XaL`WPhRyPK?4j0fM+mcX-%oworOtb zVFE_XXRJ8M-sbz*xPcBw;!qy0MUROAz~8RQkKs=T*ji&Jqm#98#z?j|#VG3A_)HX$ zu0VCL!}|Dy1m(uf=&PZgfOuh{b!=|2&L50uVuzEt;B;u{2(emt*#hDHPW<C;?|_7{$M4GDCxi)_e-4S#56r~E5|pI7a>B!V)>hQ@ zHMRAxMs+Lo&1&0lP?!wjzoalI(SAARFJUp%TrkBuspM=39S2MD}&V^w+R={Mm* zjv=m(R5O`vsUx26MK5*tOsrGCB%$flC(DV)a$EIxAksoE6;o7GD2bAVxJjf2I1`^p zea@ab_>-$`Yi@r!P75mjLJ(N9E*%+XH=rKi7-`J4|)yqF1P2_cYVhymg~{L>hd0RjMcWZ7F<)h)@JN@k_u-;J8J ztsh)}FWlt{nn+Nf5;*OYp}NwB0KMR12Z~@Hp|kmY)|biVmf42KC@3@ZROdOYglmUS|+;N?JlRrhl~*HZeBgL zwhBV3-gaJrdEK4Ts>S2R($mjH+O4mYtGQSe8jwF!G>GUU*Fm~@-b}~E*VKKm@eXH(vI}a_c2Y4QX-}^ zrW((zJywc{K%9`%_o9dk!#7cC<}sQagk1R1E#&<_O<4)S)`W~23}SSokukB2?>B}U zIQS58kU|;g!L@uZ<3{$D6b|R5?$O@Me66b+4o(7iXrTh98_T~i#fqy-n`+C%@@59A zx9>)jV`7b?JPmjp?&*MOPL4TF4w#Pp{s4D7x3gF9^Xr4-n6cpngxZLXMjIPdfC^Yq zQ8n0L!;i`!&&NcnbnoToZ|$U&v}gUJ=SmZ)j@{$Jhl31H(<_yDEY+YBU0>KXVO%vq zOG_9TgLiwDsS;J203{ez7Rr{Oult8v%Znmg&!ph2;2p+-f z9ehv|8UPTZSwLJ?3M@~9xn=Tx(}uE)p1?uBK!gXT31 z5txYgy@Ky~y!p!MHKSYe)hef8L?|)RGw@O1@GNIMD7OQoM|g+ahY4pNuV{XD6aDg247;lk&O$Y#$K zIN;QLMj0%ZVFu%CVI~_PlM^tetD{4T6slkzsgakNc5W24XI@B8ukR)uZodlo>LWM5 z;#%m`=dFsg$Af{39XknM3(eAwt}cekHm~)LS9g794iKWi)ALnr!`E_K;8(l9NabAn zQz4OuK}uGZ-)o3ri7vL+PU2hzuh+IXS&QP&{MT>aR@;OCkCU6ZSYqDG&b8+N#v$>) ze`%Gf?SC%8Q*d~58t*JQ*BmbHymXzCr;PVRA!|Ua@QQtR2$rQ>LmII&!kCqi-~Cjv z+vGG(l#!ueC-Ee+*?TN2qG$iq&8UW7LX=%#nx$xp=P5<6&b4MLex)cDrDc468InWi zQa|?{bXHb1j;N^e336f}g}T7|+A0QVfFa(hj{R^Q-f(v;EWrD5_i7Zn!^y<$p%GG! zg$90GNc|9nlQ*IES(gQQb32%}{bqAVY-oqQTq9e7`Z>!}`=^VJzL~47ZfI3QQTi-b zgS%505edo9$*(GTRXQiceIbD;e}i5WnQS33DO`dWnCx+iwGzwH0MiEdU(ZnmZ#_JI z-*t@T&kCbl|8#=3rpCZNBP*>VLe)ThWpUN@aeK*{5S}{toLE}f zrC^E(vNa`Y^q8Xs4a2^Z{4~MS+zm?6-U4~z@IfVwb>eY5;TI~4YI!pwV$hqiHZ=9f{W#>eBZ@N(Rc#yT$%b3vgjd z`K4|VvTD%gu6fZu4hurh+S#zD+T3h%qB?SJiUJM9(QQ6{U8_Pt>^6=IznD+7=!X*UmILUFN4DWvIz z!0r&X`BmQ=Eh$OU%c4lpTt2+UW|&>9s8+g5l$QxY$>A&RdN9(_16vt^*M67Mx*#;dwcva_=p>MyzKj{EZ5kMhW&l;YM|7#PWi12 zRf0lE^X4%c>&>6SU zlnAbq+>Qs!n4kmH=y~VYf{?VIIcs9$+c>Ujru?$;fUX?4wP!uquiDJYvC(K2_V8Uf zzoft{;9}&^puN_L1!GqpdD`CP#s)UgBPQ1i-QfJsl!ZUGM)u*_JIky?Uub{w@}%$cSj=k{T_*qaVF+KITdtn};I zyleeard~$iai38|x}FMl))rL2-k)VF9^eypYW+5k&>co81lv1@pzG1Bs5Mgg#PNN2 z(CT0%ec(KIhEN>Qq>K*I@0tXH79$*`0MPAedjx3n&QtL83t(EWwP#**CB)d@6VUvcS2`|v{;*t@dF&vVLetbK(;%VCXwDbOu&RqjO6=NU2=(+ zNTv@1br_P0iQr!2ny;Kq3R)szdk-z4`E7Rf*f4-TL20QNVxp#6VnUL2-Qx0v6nLt~ zL`5vYgHc0UKN5t8#g>{WcgB!JwF} zVe_HoVYSyHE;Ei)PlNLj7NLR?Q#R7bG{sqMHaN(h51YV17xzyMUHItxPHo>?niH7p z;%VDR0l6EWm_O0P%Sy&cs;I3%QX#p>FP9|eJ!CclTONpxL5>;&I$BE8KzpX%S@Z1R zb1Ia!n9Qm(BCzp^)%|HeLJXU8LBj}AVd&Mt$7{d00ssm~ajD^~AK3#BIEMsHTt8H4 zom_Y@-qFAYz_quRwa9^|aM}i~@0(Y?&rTzy{dIhPtnet&BCNms5Rpq4v9`yl%b2J> zoV_qPB`EOwY#k$hKrBx<1P2aM(-u{Q?|DA@%64k4<24*_c#0-r0s&QB7DJw* z03fN7)QkavBJyG5X~j_9$;V^?tl$YX#+pATADUs}hwDp_HTG3hRxINY1uc|lFdZMc zSt1Bywjeol2S>dTw=o7(x*tHUe;Rj}myh8JkRwt8$AS*7eh}(Eu$W%^UGOLCJ~UAf zZJ%H*%$j-4+$^`FKp^GVxz)@pCnQWLMaA|~e$iHe72Q8x{F|o$O>``*tFCXTuK$^q ztKjp<dQ4LBkbtXka`RDeMoWf=N}rw~*nnN_qhn^56(G!^xKqX-j-pYna4+OqoH6 zN<}TJl<`Sc5Oh%TcEtNYS~zoV3`Ky}v61DX6c5`(uNKAhOD6*GNhPNO00Oia(-g)* zSZK_!x1=qL>iS2dq@OtRwp%J`U=gFIgF)p|OI0TB9E1e!=tYws#AIAuGe0&jQPeHF zdWwF+rf#>Ol*X3KTyZ@k=W-Os1BQ6vmnS|82H=3me*N0<{f)RL?M|$E{jqXre|AinK8wB zGR}fk9k2Y5v@|@Fh&^jqE)8jMCuK5CpvxK>)+vUUI~_By!pIME+Q|M_LoE*VN@cRMOIlL|zsd%el*76b$I z@(?4ZG2LENO%(Le2MmKIRoyHOAobHz`Dp$Wk@;RB6gfVsn))0eDh(f;bO%ljj%(MBX-P-`xJ}(+7?xN)&OC4#2z#L zUM{>|9!#`Eo*^1qfl@q5LZlB7#?@LrZLW3#A3vUEWf}Oaki<-0MwG_oyxN>iCxIkX zg1W<6cJ0rh`>@2Elmp4b%)x>+A{#zpIU%KoP1To!1(^vs>tY4qB%_QauB0!BP~ef% z(uyq)NU(UCB@-Zz?>1uK@+^i22w$1xe-&qrA>njazr zK61&?nqi84e;niFYS7S_p~4F9e09E#X!ER8Dmpt}3@ zQvOc;J5pKx9A;y@(^xw6EIHA)xdj!HV+Q$icSW0DOi@&4k{L|q?0d!k$stO^IABJlfiwUqeR>}^Hk!Us_h*qR6e)7abL0=aR& z?foipN@k2Wr2bNfiR4W!9LH2mVDQb+riLxaXT(|J5Ah_*4OU~Hk3l*#{)~bRqj5`) znQ5-=Q)<#!xdRt<*Jy835L9Vt#l%r$X#Ng9s+6~^c`ptQDyoQ3C(SO+BVXA**I zTpagEAwgq$apP`;7RwQOozBsT7!QpJ5TT4C4^Gff%jNAzp~9@nRdHRR>0Kt#_jWs8 zJxnbPO%)C1r}OJJBgDpy2lFACq)!46>L@oUYoF`L6H+8 zRlW~gCrK5R^7=M<&1X}K8I1p^4?P6-LFE+?s(Su)*Q#A63H z@bNu5*R=@A-&hi6b_|VP3?#plp<;W(Kzu9fba(;gN>N!+!Gb+Lm+pO!((MnK?lGJr znXC-_^o&pK$dGVWQ@*W=Cq{?Y2r}Jc_zmm!kvzB!GgAMv&Fk!6rM-4djSu(?)r<@g zj0|r^bq!SUq1CiSUd7!Dc{B9YW#bztL7KjX~Ezlt3yK1Z^=}=xX~GR z9Q^E4@jB)8RVC3|4>DBSj_7-;Vcs5)B04Ea9fE5G{4ixBK!$hHZSa-~Jv>ZbT6A|J zCL|$9yBi(#wt!P9E3w5+57weOBqg1%vS#tcW$g`m5p;d0*nCIYrKOE{{}pyXDB`MZ zMONEWQrojLI6uB`?efpRq4=6_0Wpl9_=jgF$BT-0-+tim#)TzoszvCKfHK1R;Gewr zE6l5@qCKVP!KIm_Y{9>truijrFDO&rSn3o;Hd#QClHT4*5VX(whzLW>g^Wdq^DI-b zNHwt{?R%j{FnJ~fjO$$XBeRxtdc#h@Nh;eDP!Im22aH3LV-Rcp^t~APl!&N~v)rf+ z-2p|+r33X#-CEDdJrbQ97lH=dG?!X4lv+(5Og}FxP~z5Ye@{skiuIg>3xHJUgC2D? zwJo(Rm<)0pgoW&TtQub4O3hY>gY^1@DtUBHY;o=G9n`zAaz|}K?H<_4{?_Oa@r{?L z$NjbED_e^E44037!-n{x0$)I7cdk1P7C`>~TGo4jK!xfWJ2sfKiruzx>4 zfRD=o1^&7PTA#2+qp73{HZ{{g1m#dQrr3FSbp+Ap0VQoA6V{e5TqfE{rrJqgPrEoe zY*rY;6mXZM>@v6hz=XznE&omK+l~KIlX0YuzCpsK72Cs-D)hFju!;h>;~uk1WH*U_ z4?L-l?GXptNU_-5)c8_lXQS+9O)lCGTX4<60S`)qEo{NkN^75W(hYyEd6W!!tD8uM zSl>D=l#dv37%ppvN3Qj=y-^cJC!rM?B!R^Iu_9l;rSQsP3 zyr;sX5aC6L`-+?`yoX(#ckO&noc799?mYB+Bfghr=<)o*Q`-nIn4c8s_(!+tUhn(u z{j|lfm!Ywfp&9+kvBs5_J$;}Gi@SxVtw*SN?rZ;E+P)A!cwF4;ub}69;mQ;y!_ta4 z#2iO@Ci!YA27Ih2C@78nLCG#>Vt^8s0a@dB;752dp#Jvhfd;m++{a_Mz~ZYw!3VdU zb?*X|8x@GQSXRrfJ(c9(ntSrK^1SO#VV7+Gh9Y<$H{-_%6a&7$8VQFs+7jxU4cis5 zyM=%h$ZOL4sq!*{3PR3Auoo;kAuYQEo;wMxRS$4)wbVo zbncC*u76G3uSs3Oilvq;27|As;jriFFjA{ zCwBgfxX=MXQb!kBG)5(r<*%O3dc%9P%D1|64?m1!-DGnfAr#Mm&ki#kZ|oJzuwbSf)Ix{V>n=^19V6jq$Lcp^dbkUhk@}iQ&_PeyPX0tyeP1UP z3X6HJ)Ib|1rpDA@8 z2$;$k7ppBd+?++9oHZ`;%cFWLb7vi`!H%NY*^E24E6*k?57efK=y~Ug;N`Y-+`)!3 zc;L0_RG6+D4uC#?nA8F8X-L}iE}M!^k6PS)SK*VZafA_=KTzF`9a223aMOG)Qmix| z3T_7gyS1>UMIUVI1X<)wuh+IKqGP$s3+W#E!v7ZeDO$-sb1Qx_{8DLSyxLGB?U{D8us+kHlp+3x2hi9ZO&4Y%ma*@wP=L=V^&8axBi^{L5QjF zwbyM>diz@)k-bDL+b!yLI5&CRa|OLgPK+875&bRO(E6NVIepk#b7A&miY>F@^6}OoTR4zX3?ghZ8fScyFDh5=P`Zo zSN8Cu{nOzrAjUyv>SaYI1~Bks%4ylw_kgRm&JMRHaVU6dl&XIFkX(B~s`&mFOEz5y zD(ZQ2q{880Mn(z6C#Cag01|0fHSxX9X<(lN9l9)4H7k3He4GVh|JbbS5TYRVy0AaD zP~x_0gn$SeKqtQjU;p@3q%9t%H9cd7I_J@b;|lFKMCTh<_TidB0}kUxwo?R(#IzV} zOa-GK275`_|J|6P$Bak;=^U#__0F^*_k)Or?fp^s`PcN|`n{Nl%J%b5)HJ%3s|BO^ zL2k~EkXEunf7k&2#Ta4!(Lor+n?dqKUCPVck}Xc@nR2^zPWYz3%f@QP;=TF5JsBb* z!G|RuOGg>?sMlcOlXLo9&wkaRX+MPtS3?g0(@0r(WT$&;SnxEqc~p*DL=4BYd2td7 zIC@peBqC_+{<~4;^2f1KF?;TO&8l5&c0!_W(V%1~f#zwlwp(SpSwt$zds4%ddWIn^ zAzf7_tjowB+@D2$XaNn zHPFpNJ_OGQK*K^|ADDbGiNRCuXqZ@O2`>8E=B1Ln*0>|vQ&f;ofK<*0qjl{WP4JH# zA52UIDX(is-YD;yPtOc>0RGdB5K>ZXZt;UwPJEkYjV>RQh@`Vd6`MI&lJZMC%aTf+ zK?ZH+`QU*rqf&svSfPZBf{OW$nmc8#-ROU3Txw$!sG`E2KUD%w#=wQ3XWs!xMGa?y z8WHGd-Us8eDr-P>7b^!D(V4TMzMMXn63@eK;Y+b<~|yE!?UASaLv%r1-^-Pj;;bm^(6<3Yj)D31!SRl z7(tz3AUUc{sO+(`?6m|uk>V+WyY~VE7|{!XfSWPLcNwwusGrq_8_M7%_J7KfM3jj* zVd$|`rtG^4dJm`ohQ!Ca&R6AzT1iBR?mNt?s3NyoTmEH@`dvynD_hFR8lQc`whm(G z=A;O{p8&1`Rv@T4Et4B1S0j)|YD$%O_(<I zpXMO5mD!KlOa}uKvIg>_^C>;5GdtmDw3ia}aZA6gd2r5@B$T@UT42UaxOADbeA(;$ z&kB7I0mow1q`OisPiP?EY<9X$YmVYr+afB(*FgB9`m21@sPqy^|rm z4cV_>TeZUmIc5v@O8J!WN8@1~yQ#9rkJKLZV^<4^LE%e9pL)x&dXCzt%t2ifDBfvm z2LCt&xq4I1q30z#=+56*HS0bb5Y8at_~!Fh40vJpA|f-9d>tE;phm z=iC&-tldBDc2-I-Mp5l5JCBm#?CnX9$H&9=2+zc!9x>9XLJ5gobDqCpZVJAa>ze-a zE6!h^t3d%`{r(B+yP7ZZ`Y&M`kw_}oj12cI)`zRVv3c9alWBYHVx%nF@sbyMvKX;i zak;h3Fpe%etZSRh3>2XOuIBjv6~m8V7;G0B+ zE3BJ=Ph>ds=w;zF5&nF{7Wl$7X34RTdK32J7`{Bxxy4CS3)O5vsailDIVF0jiPxat zvOaE;_AiaduZj|;Or;X*|1LM84+83K@^m{Bk2xb!ERqx&MLC*-)zNZP8HIT`_H~?C z-(7Xl>MVYjc-ISyrGR*HCl(Bfmf{yY7?9U}=wx_O{(@1{=wTpJRL!zr4$ETkmACZ= zft7&e@O8*3w`n2r=?q>f`#mYQemYDK2?nUctN@0 z9L*#u`e`LlGOm4#v{Vf{W@xl}EQ*(zw{0(49)~2?n3*Cqr}ygztRr(ay#BHbXdpf4 z9VbMO#;Sm^5F8wkb({u}m{N)O58Dzzp$NE7n)>-Wh|=jzn5!m$^Wut5(86^yoD=~M zgRtNr&j;|YYKQ1mn6Ve`?U49P4RnW&XmTP=R3dpQ;PBhEk#-Ypj?0r}jO~Kk=6tTRIa13(Jsw0O-f@DEfoGMyeLPh`4(8YKP-;rzO=~fR!u_YH-Wf zdI%IMu^wVc)$Hv1o!$fIfu$zAp4kHl2mooOw6c}>F8KX+Epby9emoC)ppM8@7#kM2 z=vF56+Mam6bOlLXu)NJ_LTEU(qAA}UYyXEyc^0tlzcU*y<&~4Of9a6!6?k9S`rPu5 zz+82GVXC`{9(#Vi2=FHboO}%$MWrH4@#rUm=zH(e&2|b@AKltn=Dt7XyU#L2tg$#^ zc0mDq#k#PnIPzcX#|7jNyHr_ci>B z@7kN_m%nw52fajVzEg^KzHbNqyGY@th0ehD}RZDFPk7z6|4fg+vT_Vi$R>Vqa~lNf(4&JCQNA||dFae+{5 z=-}N-GrQDA>=MleySh{MmA9Mnvu!_Y*bRicm8bAn-{U@EKc3VjR+AHY`axj5NZ5Pw zC{uIppG0q12O>dnj7^sU7c4L&HvvY=TZJV@E&G-@y|5q|;>W4Q$EW4L{3v5p(ZK5$ z8;G((p}_H89|N4h$? z)2`MkHt!(VCd53az%0Vt0`t-=4Yh6t&Mza^PJV}kq{rSjJA3;(SGg+&KmerI>IQt< z+P><6KXYOsJ;YAL)vJlE!)5L9R_s&uQESD{7v#yrYU)tX(yDGysj#ZemRNJ^Gm^pp zkprl}9E5jed3(x!eSUjUm6aW!6?)}QOiYxnY|Y~G9`m+)*w1w5;F_lWS+dm^`!?6m z&d$z`0z^l@Lw{cPTihL>B}E8WCaxSE9UWVBog2;M_sBXU!@ocE_dlCHn5v})mhW2M z-~-z4FEw9}m0wNY+FH?tuFvmIThW1x(Vt_!w>8o+dS+ayG&F^z-p}FL0uQFAI(N?- zd;WWQWWHlh!sm@+KD&kKcH>qYeKBNjb8k=fS67jFH}F9l6>Cr1(cH`HE=l(=-;(`b z2HoGD{4v8}%V#V(+Mh0BTkixL5)u>PaB&ShHZDy&20Yt<#pR_ZHq>F(mw1hJGV)?;$EZpX+eimeq&Fv@~5iKX<;&wyPL^ z0h6rGoDIX)drZLRu|oM}0I!{um-A+SD^^)SVcBIX`pkdr$-w8Lz>=}h2ov=?BF)T9 z#Z%i`(42vP-jjCn`CVlpAvzae1Fj(>N*M7ntOZHC0 zcUkH7^;(QDK|!=inS>-HObiS!tM2Vfvx}Ekh5|Q7nugBvC+)6#B)Q~m&YQ5Uvm4?F z?ryHU@^Tpk)P1W$NlLQPv7`&F-g9IA7udGs5u4oY7dJ%qe_mbgQZ)^|nX+Ws-e&Je z1sv87EV3wq&js%6-}Vew-63*HpQG}kEI*o58J~M`vc+bLJCjgEN*n~{swGK%)jx`T2odH@Nh^+L3Hv`>c!@JYQ?sLY+^lfQzX4Q&y zArMF*gQBn-cg!!V_54-eD;~bJ?g!H? zK0IeXUR%7MpaTS+`%Ug%@7&v;3)=6;Pu%;f73>=uOAc7tt`p~0J>EO_0fsGmq@jN= zQk#Y6gjoRqUGTYI6Y_IqpJ(?h?+woeZ;rXLz2!z)zst{F53%j%((bE{Pp`gDmORZ{ zw@dCWZqFxgcXwmYm)eW#5bJ)PV)4tVs2DvVeOb3{AenD}TWf#42_^H4n$0>Dx~i-6 z+o@lB7CD87RK6)E@7vcR7ab0v#SqB>=-?lunXi#I(QAYx8z?oJ&V0U=V^91AI}rE~ zBuN28>IT|UL9ycD?XAlZazt|)+lGA7?n>6;0h1oo$=rzv~%#2e$i=Dn8A02P2|;Pb&f6~3&u7yfa}+rDvuohwToLac%z{*$wG z_N(!Pd-bhtp|P=#)SpuG38Hh%%*^zCd$Joz{Ff}2v<)qqDzk}8{^czSVOYinL>rhx{w z06YH++1mYBD5)P+dvpR`7x1gDfRi*9AxO05!;~TdFvZR;073D&G6a|6=eY9f>fnIh z-oC3!G5)_gI_Ya3M@%|D>6ZyA8zIbMETJm5mp}51kzMJa#f;qz>F*%3v18KgryZQIU7T zMK2_!Z*_)}KKA;*CO5fA(!@^~u?pM`70uuPyT@TA5fi4c0rtLEM2E;g%|IFRAX};e zaQ@`J_5yk8*Ot-+vahrQzhC@ffqJA)8SR2IIUL*FxT}ssdB~!86Qta;V)TxHnXvbQ|dIYe)1)!+;SNl2c44 zK;Hg)TyC*~cARE(jk{rKB^o-2@s`$Vo{d?ukYM|t+XUp60wnHDFexyl8r2OfmpyDc zZ29?X!qV!NMbp_yqmkQCo+GZJ`MDt`g2$c)njbXY{P^&qc+7Hw5CFa@(i2JvI-RWF zi(kbxK70Gd0QMIY4g-S$IC+KcOcZYXOL4llz>D{YitIv-#6=2&G{bv>rsy@V-;Ctp zxX6t(T-pN$krVA$bw7gs>`&zm-F_wMP2E)|H9!rP2==+$-+}K9EV+E#NcY9S2nVQ1 zo@I)-y=nE`u)%4Pd4a}EQnsUa&CzXN*Hl}pta(r_jEsf!zIXJfe9YM368;#u5qCQm z*z8aekPR5VjRi;m4iG?8rQEPvR#RbLaem93-G5kd#TTOZNgw(=fUPb91oQ#-+5y)OcZjX0WNhBIS5BED0hjrQ|8|Hj^KN^? z#m_R09z4`uoRSiKkv0WwV}H1qXX?O4KOsm57QM=$nw*?i=QZf78WA@#()IzZc>?oH zmCY1Etf;f`gBWntrLJ-~J?7PMKDA-Zs63=JE@I0GDD zjW-~Wvfb|Pu(ErY zQBkUeT>P2G!%^(R&B(*m8ixNBNDhw(=@4#oCvVs-1eI4ZUW;_>_R_(3EuiHzooXRq z>6E>hgIic1Uf2QxP8sR2PZpnGN=e5c$9u_j4aewqSKGcG;a%rbU0HY%wpU8w)YLR` z2DGVU`kvhEmEPaB-p+x((9Y4Z*NpFxawUMR!dvTHbL3SAYsTcG#v*q@nN&Ga~CWewX`Rh1$mALP6 z5S8drTfY-)CSC5~^!>*OaE z*lmK_h&RpFii-3;=uc~1LJ7JlKo{m(x(%@brE%LO_OOzdNVUPl=vw?LX0F9KS$LSA zS0OgEv^FWj=l^}<>wVUUiyKxSsVPaJ#$-$|`-PmPL-R4IG-{8a1-zv9+uk2J{-vPl zrIG?|;?%vc1!zMB!u$MqHI8oOr$5=WQ!@z=R&iP9;Mgzmc*YxV<5ben_gOPs_aQvm z;XDqF&sMJUP?gcPzg0`1>QKljs5t!_eNB~_kO^#3U2Aa z6pu7pk0%wot-Lh&Ckx@`isMZ=dAYhJm@F=n6;}O>cGO%$MqOPP&fonwoptgp1Mu^h zS!F0CS`xUZz&qSF1QN9^A9LM2;VQb}e`o&!Ey8OP3w&dc-!DzxgZ1B?(XXHyPQ(89 zTDqN-{lAl>hF}2A>6-IqkMCZ&LgPL|YUX+V_Xb6AY2ph6`uJVC0DG?oDY-K8o}3x&hsG4p?VdndlXsL&A+5cp=K^LuKKVX`@w z-pWoFQ1>|zd$_8M4Ad6Ne z{lwulcJt#E%uP($pueh`8PswTTsytKeNJxBY#HZbVrlv1LqTzA0TS4X(QLKmEf(ug zp_eu=|Axf#zq@wV)a4!O#b#q;^9~O38|34P>@tb6&_XpF_b8QlT&qTuZJ+sA7OrwrxXdUKrE zz_+ziRztC%w?Nx)95^YbO?u8Ub%+Vxe*^^PQS5Q=i<3A%UWlbuK~b55Wx0xh-OIAk zl*!+3!>ED4mdIJU&p)@{Z~bIQHTI+%dFlwOAig;M-%_=iaTRuV*B6Xe=-HXXLG9wQ zWr1?tGA`wm&j1jaZLhIRh)>$5^xwfalT)IcbcBr`yrG6{2^F!BPBy@pu2ZLiXM?d+;5W>#L_S7Pr9$d%>g_u=sL z*QRo9uTlOdCAu*mNoK8_6h*aEb$%wM(BMa4U|?W9jY{Kk;QTwZFwW<)byMCesIwNz z_QVF58r=N7Q2t_)dRZ#=h^C$^iT_EbQhC86U+(=jeS6#g z3Hbl_;?<6vM9kwKF<*IFT7CbS>P}%CP5ih}xKr(?=~FKLJx>3V4@{?mbAB^#ng4|B zRfs+Q|DPA~)GtDxNDeGCbHDy)Mtn&fB&(p$%SQ%Wom?Ro^ru=jg*WPKc^^y z6_V_4Z7vw#N6bOU4o39qzy4-S=&RpB$n3Z`hN0bKbla)N?L0wEZVOmA}c$ z>o(>q_W$iqa^4Dq;3KRbLYfSO2#dvs{bx9=$%x6S6}kSF6z2b99$A|E^*$0 z@qr$Tnu{~n#23V**h2PM1Gp~<_BP=92H*KbL@M}mOif>JJ@si60*rSeYg)Kkcd^Wwc62{Qoy_W`l5dQ25v3@GEeO}9d_@N= z?W|=Ll?fxeyh|nr<~-gFf1omI82eLJ@TRP&1+d*)-0T@phzgeYD+={Rzwv&067y5B zyK6v8M!E;NEn&oOsp2@H=ecWbjbnzNq@e4_W)HJGcsPv+6ncB>9jQ#AGBEfg>dQZC zo@<8Zv5X+#WfWjxK#A7E*y5X0tYxeaTX|#EpQ%mN4Hd)IHFUXV zlF~yv`<8b0f1;x@52s^d2OY*LVSZ4^HN&&m0_nsugRRZYVT&hde+6CEgy-~o(u5!p zU2KMnVsKU^+X*z_-fuzX*}1* zF)C^({jdr~VB;(ngrWp+Jh=)2*YA4#|MP1{8+N8!#?x>+~4lzdK<#z}b!5$UtOd zGBc6Fv;FH2Ibc4~qu|r(UG)W@kEX7vpQelt79c6vRsL$M+(ea^j}&=yW)c|y_b|c^ zoCQ*)kedOF>KKi~#%9rQx%uDaYgWOmKnV}|tr1UWHCuZ-pi-iR1ytAYNK1BIU1Pif zc|wiC`0?sqFJk?OvQ(F+{e3?~D(4Hj-BBxXmkgah7rjw5cz@EciVT$hWPQCA3A9<{ zOqDi`4Cqfzjqm9EXP&Ewo==m}lY~)Yh5?^n0hk`YyxZjs&KK~KDl!Z|m0_0XX=wzmG(F z%rlu(&H4nsjFv9Q=fAz8$rEd`^BuG0nl0wJoi^jbM{Os@RFamk)ajhRy0*7Jpp!xZ zV#V=Hl@#8blai^A@t^=&MVdu0SyAXT%bsVZNUe#AfMLXc`k)&lgT1#IN!LLX_4KWP z$yeg0r=g?bdFVxip1w!xrH+Etq`Y6J#06#P?6EtxyVptvcm$@63>@bxENcyHyXDiR zQ2;i!3t>K8)OQ@%+~?Ax$p-cf%x#{xy+o|~5PgW6=Pstw_@($6YF#f6n}-bBZSQ{q z6%_|8zyC0>0Hr#8I!*UOjtRjzRBml;{_h6|DRzfM@JifRj2g26WTE3PEg^~@QI(am zZn<6wdhqa^em7fW2#w6-PwUQ(1&I=^B8{9kus^Bc8fB?@FUQ)AM?n+Pkz*1<<5S1b zVAPg}L3w=itx#G5u>s$`Ke$iC5?^>!WYYntQ53|9l%gFABs-B*vb;xL$oE?m_xDCqL4d_57wpQxE3; z&5TLoN?Ll-$sNlND>a&qWi?c=aSHk!pYXofC=YP_LJO^Jq+y})-hhw^>!tdcS8fX) z-+DNZHEsvKFn^t|UA$Nh`pQP(IVMU%BFYk&b+w+Ig1Voxb@#~+5B^DQAMa~aRMbo% z|1V7^I33TLNJ%MSv;PFx#b!)66+}h5Od{HURp}M8;ZJ?({h4tfcVBJ&W?;(B+i@qd z$-AlPFHN>=q_)0adjs$Y5A224%T0_Of2nXz=acNKGv0)~@RJC>e=7Cx&BWN!GEo1n z66frke5TdZRq`bsFM|k|x2KMiT2S<`(Qk(<@2L!MN06NgUjN{puCo z*2oJ1@5v=plzUOlAGr}sFH;YEB=g5Lj2LmAMVy@+gY(V8tg~dB={!pIu5eO|SF+mg z2*O20{fO3C2qO$)s1`x!k&g9KlA!>AU64rvu0}As^#h2GvP1@K(P>W=#FF zZ50Jk{U|Sp*!&jY7iei*+giUE-KQ~3?%1~mxh%$iNXf*k;FbD+(RkeSSFTc?blSdWEW>=H|2+`cMlD*G6)Y$O-)&HO-@e2U{ff8 z-`G9ZzG z+1WV(G8uIaz_?2AK)%`Je<-mksZ*2dkLtw4$?2{3QvMHxm!t`dP9`v=+hgAr%#I-NW)bs9|PCA|F3rZGK%6O@St}mcgH5 z5%;^F8gGUmEe82hIL{1Crt`}1!jMQZuPvG*qaNdkZ5zzy|AE{Tw`M5(^mO+ez1H2@ z7^JgsczQc;od_F!_@J#_HboW|{=@J7o-5gpo&;cIWj@?H9Jk~VTNLZ)=m5nkZ)bQ| z*(jME2ni35{rZ)Mk2iU6^Id2*c>C_dX%TRLc(N{iLZ7Wgz@SA{mq|U747O zh)+ZW8_3A`_v_cM9xA`@tYC~vwvt8m^uJibatzL-6a4P|SW8MSZD%cW?1E8(rSeD0cUW0jL-SQs*rM!%M>IqD5`%A0B?6!UM2O_`nL?3q}S^@yK4NvrP|UtyExIZnczRL zm+z#3*s{f-MJSP5VMmX5b(3bJ3?h`f>Pfjy70XOr4?zl}lM^#C z=6R_oonSzZ0u)SiO!b6kGsdsGFJGLTAOqUk5lyL0on4`Hn1H2&194=?j+Ip@zP5P7 zQvZ(z=WEFa4%gtL;t(j3&ikm2uUWy!Qc1svfe@ZL!kM6SzJ4(~Jj3sYwrf@DAFn_H zKEJx6>zR~vDV18xQ?$=Gdxwh^E*}1BY8KMc-qN$%y_UxfRRf7E6yRisZ7N5$rKzbp zYVOlTA3h{75r7_itl{tPV6-%r)8kxOwYCN}c|*Lsq*PSOPW85>spl6&tWuByWletu zahyHD5Tupi**$AF{>-DC;>%%FRCb9&o0m-LauI50!-;;&pK9vIIx9weBt z>sJ4%tElqSU*AxQd4Jb`7M;Vm*vT_J@8Zu(Pv1u$M2GpAMwU4B=PT-2 zTuv2nhWQt}%1v_H5n8fD(hwtNHlF*OAhAd~#(31vw@tC2fpTBj*~Ahi{m%>u*YupCpt4+5GpCU#0L-<>GlvdklY4G!O?3Y1U1?Hln}C zb@flijHiCYK*%5ps*g`11C^^$=?N+_V884PS4Tgd5aC4%`9fsvsmEVL)~=2J)>9vf zNqzQx(e%T|d>d9Io{ni-KC+=@-m7OFVKW2+7fHlM=Ab<^VnF3|;&nYMV<4gIt3oz# z+wW38<|*Zi2rWN*if$IdJ4m94Y(*Ch`Aa!)B})Kc1Dx0W3LMy`IWJT4UV^q837I79 zRcXP1cq6RaNYqD|3owv={Sq|uXEpC0JFv8=Jn^teuEnXWpXBFaq#BBD&odTb}U z0AL7sXJFQe#B+;h99zM#An^`Yd1(02rcW)@oTaYrczL^%moayVF_XDfJOKLVq4uVz z9N6!^T?x^Em(KrJa~RfZqE8Y;`5YS95QdAp;63Qsb8Qo`@tD}-Nl-vF>~fgm#^!jd zV<$PxyfSa=v)|?ZP+2FEJ(nc9yGlMd+9p4p zAYpv0-MH&^Xn~Zk%c|v?ADV46!@uaicwBl4M+OcV%fzd`&6TDP_PJo;^BVxpBg`-sC_>fjn@ixdOAr}*q zMZ|(bEVu zd4%IoId_D3{-|QV2}3882$txG%=&06D#^d3!Uv9K^el5Ytr#1x_MWv|J+~7>d9(O> z5Bl$eI^y;)fMu|cQZ0suhX)M<1A23!AdVP&-mqcMj2~fkx`{y^^+HNCOj8R~q1*aN zF+)sY`6pfZS(gx?fVr-g2K0pu{)e1p{r;1anvmQ|6Wpd$9wu z3JC*dGBK3qIcxw4rB=)7un}O~5$0ywFOImdS3?x6|NdQ)>7V9tx1EVN!k}_Ej9~qp z4_1T&h}S8^%N;JUmhPWrj(C_k*?L{AtY&|%ulL$ttr`)57-*FHUURuSf%gLL)M-Kj zl5xk4TV}wG9HP%uxGvy$o_nNxZoc}Xgp*o(Az{PXT<~G*5tKXh+BtbJZq;%72#;TT z3ge)a7+{R_y{nM1vDs^E@<|)`otxKuNp+jidKXMAl^rqd3Z7QDbw0ymb(*a8x(38C zqi5|gvUaO#*gVGs_ZI&NeRMWE{^&@lRz+;ouE9SO?BRyt)zvlNS7l+p zU`1Z(utm<+P>qZ%@HW0PK}Dp+V+oPd6T`0a?Rb}&y=tdVPVVq-<#KUxkt(3ECj0Jh z$8*uERjehK;x08cH6-B4@rCSbT2>r@>)-fvS4fCw)eP9nUuSl_O7*ZEE|hNHGraKT zd>TA6VuC;gcx(8sWynIm(UnP`5=dmzHPF}R|D!7KfxQ3C8&Xm-s>Nnq7NWAjA)IGV zZ?48i(y-|0>F=)7#c!_rF5%)6)1$r*ewT=Yp{beTiSOSZzaqmhBR@*Hsa`h2XVYqn zFyOY^=5oPDMd~^yE+QBCLuVGzhEr+uJzA*ar zJ_o`0$;k;S@WXmgaLl@9x0;kMN(#5r#-UkIq!=}Dd;3q?K1(Ex9pBAPeg^d=GKi$MCf4n<*r`JCD9ZD+!ml4yUcF%lu$|Iw&aUYqcvMKc`q$UhS|6 zCvm3e?ZFbF1s(;*?!ZL-3~lmYgH&EEJ=`Duv@R}g7Gbstl4aycQ9RuoI!zd;^E>WZ zXGlJ352w0E)H#ScKKR*am08={GqJq!Ml^h)Xc!SWr&Ai=RXa}kf4Kl7{-@Un1QOu6 zkxMo2aVT2A#ALp)@m#d}C1B`zi~)o30!P!dvA(zZOJ-B@%8G-%y#kI$qk9f5Xh8nS zsrdDFM-QdfIvg}36_0*s>cAb^ft9uQ*CJ=;4;IiU)WjHh^4%LBHSRmf(&Azk$p=hS zYCm@@gsTjYGL3KZ}w8YNH zAxjenqE<}}gU3-zch|M-)9wETEl%6o0_$R-=BJ%~B5|ref7Aega}p~^E;U~JxgEJy}>guU8?L+0eEz(Nx4!2J(0s>ih%s6#*eL^xu+?fGiA|kaI!=}M22BoTJ zD){%hBp?9VDxYTXNYlcKkShThq{rJsoAk_5XmS#5%-dBv`MaJt>5l}Y*}Pzv8+CdL z{;qod*DR58-)+spF{#aP}@LQH{^%PA_E$*5jR+#wc zG706axA$!S{Dy5L%-Vb}qEsfBXg_uY zBPRJX^{Y90=H^o23%Rh_ST^z2<#4d0-FBw3S^at-k*%n(@Fi*Ev}o#s-^0DfqJL9C zLqqwWUcvMQmxR7;Sx%B@V0q=>*&{%2LHNrga(j4q0G@07t?tTnUf)k?Ko@7zlanmx zXNlWbBcIxdwY4+Tw%022JJ|($c%k{L#R>R&e=XGM<{!NeE}y2I5-UIl#(4{MOK~LP z4wO=HZs%?U8+oQYq^v|PDk@84t@kA^H_L6*z};$6>rEGXz((3LU}J+2z|F+XOo=!6 zpjSAxx3{}H{y|;j_kk@2JK%X7dT6K9dSLW$tzu=BJY#!*Ugm$Ww>D5JmZz2U(nmQqIW>G=G{Srf_fVM~bRR&fl0d%K}1`qgJ00SF=3-P1!#7(Vh zNCCvjfl-l-4MhZY^yLI~$f#SGQnaP}Ou(YsDGjd_7ofVt>bnVfxU-HJQzRWS^RHP? z_PC!}0g>uVvn=%C?st}CdOABRg$AQmnO=?Au?b&S&)jWpQp>t7N4D*kZR46z0S9`w^Fm=5!BU*uUSjX9N|`H=hrffAH;>gTehUC9({d%Vp9BnGq4TDz(?2c zvmzHgOFz3%!nJXDqbKzTh#HUrZ|qkT4*MEH0sm%Quf}2Xym!q&n{`bKqC54`u($f1 zp2x%}*XU6Y5U}D$Tu=#k3I==nU5!0F8cwmB<@>uk19f$&|s;WmF+J<_yu5w+WSp6?aCDPqom8l4!m+j#XS9?tl_fz7h%IPPkr=Uh` z`O1NPkb;2iT&*Yr^WK8^!;b5iRYa#IebrPQQd?+YVf?>;;gs{_r1-OoV?<or|@F#yE($BC5Bk5aoA#3M3R4pgR;Z%<5NXyx} z{7aK6Vu+5p*0sFsqVr96D)EBzPtS3x&JD?5^xf@eeFWbR!NumA`@o@6nLY?%sZYb6 zx1K%S-DhWKh?R74=y}tU%YagTbN1>66+$&jwb;mGs`9nMa2k{U!Q4^?)2<0PkxTuB zrY6LWCmHyaS37CR0}jV?KSB;{Mch+?L9xi`M1lhp(b#V6c|TNlhQ20B#$VZBYVBra zB?yp0)*WsdIcZ_07aJK;*!%^5A*rOLz&t!xU ziq|U{tNJ0|>b*D!79>e7sCt~2o{<8UVdXPl(zH8Lci}lSLn=eYB;B?Zv$j09l?12) zmweYp_Yd8}$sJ;RsY*CFN)D~3$_fg5k`V((y94Z9-NRltGY-gYpjB+WJ}*mTG5W}t zDyXXs_gT8XNDQZ(ZLqnV3-IlUy9yBZc3ZnS5AeDyo2w1j(YP?F^*?a}=le02A-<$K z`Osie2i9NN;Jg)p+EKspHdHu$Eu;)7$h4&#PR?I4+&0wM`}>RokG_s2Eq|$5)(=#L zc(p0`wub{WE+M_je_S*tCud#+jAbI;Qt$8C*ReYuIIL>9DV47NeOk5`l08lT?*9^G z30BViudl9V^&RYu)pOo+R{I=M0=Q44Qu&G>7E&EV>Nm!iQ|3AaDNoMRc_QmvCVBN* z4-Qns=7cyv8Y2=C(%wO1ls@vpQwRG?Ic)CIfzg)xL26+`?bvwjSvthAOl4Fx$70^< z<`AUZxc&3sSCGy5wGq_hXYME{Wh*OL&&4Y*?Z>R$mO$?y@&5DN?5vARQo!lVr@Qr& z;o(w`hl|Vje;jU+B^Zlqu3f3`;gytGTknlJW@x_i17Tpl)!5UX1@S|zA~ zcL_ap_H!3gt@j;qXuz1-SXuMg>U@KXw6tcX!`wyJMHxWN!n1xlB6dIGbGTt{ovR`uAs=*4ylIPu6IKQ|o?oru8 z^A;mE0ER<`i1w(83eGn_yl<0?{2JYgT-X|EG%2!$*}GFF!Ees=UL^zFV8Fwac!L)h zVIEMAUmkggGx{f><%yy=LgqrwY9Oi9nV~%(2PbL!wA z#c!_G1_#f+e}Az>*m`$Tl)!KbEQ1c|yj>zZy-Jd1EOXIIpM~h*dZ9H#Q2lNdi@mrv z{pYFu^*{4WKg4y@A%c~b_Wrns%1v)1=HoWFf8JBGyzTGaeaw2rtX+K9X@#7ae8ERc zyLP>mvA27GSg#tXswDD^jSXQVih4NfsZ+8zWe+&L2?`ExIfZYfGb~G3(i_32ldC*; zf}e{!=MEh|CbZa_WutQ4OfHO&Nr2CB<2<d>CAnA~%%LBB)GWUIsRvR(m|!KDvpL@AD0lp zv7(h5JNcQb>KY0*3LjTGzkJuH^mm+vLOd=Zrq<_J?1(n75pJ#*Vl?9uTmq@;@90=# z5Wi2BE*csT{*jjExdgqW6!ll~%l(hD)3c1!HZgqO#<7n>7~`znJ~g$K3?gPAs%w69 z^z0S{mDV+3;h`RG9$=*zAW_N-3Tppy!~rlevUu-w9Xi~vuib=&fhZ4zq6~I67Aqe= z5=DhZHv+$)P}8+Ax0r8Zcx?q5rYkEe`=-yLy6!SB7eOcyfDC|i;>c6Ak?J}m1Y!ZQ zus>wOqqOIGaop3<@mDThq}+kONmi}7qC(CE?J-BOz4sLZ^{W5g*KY$ksMmz|`Pn&R z_>15dgq^HjUAw!x8U6=e%BzYsmPYPynU-f;g_(>J8%G5(cRt+X5-eV#GDr7Hq8BQH+ ztVVaLMJQFMah;*pDg;kLr4u6Ph zCORB$FQ1~(N!8TUptRksujg%AY~lRh-ET2p?j6D>cd7y78olDlC>%U5c&$I8>-ORP zlC120bG{+od>|1M^^U#e?kV7ZJ4uGzvRp(V{3QNM%-8vffknM7W3Cjy_`1>@oUjU6 zSco~32#*!H|IdHT9zcDvX>z~}MF3*JNTt|)Mwyt{CBb5G?jGY==^zAgkolmYs`LFD zE<66y>67qSa=(k6eE)~Z)VbE#m4nTTURwP{pNHU)T7NOC98h;~ZyIUPe6z;>Y|$@q zfDu6J70iK#hQ{!w{ZceHgT3`8U?0$H^@qEKnrw#>x7_ZJt5Asg-+{`vUhH`?QIN5i zVT8ELpV7ri8;_P=dv53rGv;W%`imtvGxO+arUZ`as**1*?ygTk;$y&jiO*C%>nq%! zB7e|c6mmfgAO10mlT&E=UCu4(wc0P_XWHGbR?2*odd!JY#P%B07q`~=c*-IHF~jHf z2bAw#t+??LmwK;zyWXa}rk$b%y$w-}LBPO5ecWDORy)!|n{h-ZA>b*}qGNkl`Y0Z+ zLQ2^w%j)z$RY`@&d_c|`GEuv9v#CZWPN4}ps%S)7w#0l~hxbz$BW?9D=dnyTyr>bdl5!6s@BIRYJ=aFaoVdF$uIh`!AXXL9vqn*2Hk%s^;0wqc zAjWlF-4?U&F2=(r5)|h>J&E0xA5dZDws``iF%NH9%p*$AMlIePZor$|@9#7H&QySD z&!xovlT!*fYZ_NoWK^fy8dU#cf4*?aN>Dd>1_apiFAq3rcwV9ae5ui?zB8U&UTu=w z77v+DcSzeVY|*23zqcnUVkS4y1Qk5&A39AMiiM?Q^UeDc`e*xeq zXWF8uzg%@rYBDqz#$CZrE$V!0UyS-F5eAPwf8A zAsi`iU#h`0hqi{7FsMCiCpl_+XjsSR=vht0~#w^7H0ndyHzMr!Wvc>+GW&s?7VwlM1%jMzH%5`|E7<)0#> zNaTs8f%hjOrb6c&cY6)0!e4cn&!0Os2NizF7M|VhB<6Ydpl@|CFyxy7Zy`gWSQ}e2 zo!`p8RDTV%KRBT6==e0Uj>Cjbv5}urd!$%aEHS)l5Ztb6=o|)79O@;)4C#{cia|D9 zY?+Bl1dHO^=d)PmTY9Nvz%r<6L}?;_{i~wC#-wxe2Z>%pU)ovOSYczuIwyI@6i8#K zP*A8xBLhB^A;@v_;-l6zX`(KLO;r@r{L^=5)Af>KM$>cSY6d?)acs}P!R9nc>8{4c z#vf^{2JuZqK!)$ZEwARr>rYtu3$H)X$*~#MzTpw^R^;W{84_7=5p;2iW6^8;a6l!H z_d!#8Hkjl;#f_|V0a-k@!VcOp`h#f#q}!)mq3LVzSf?jUP7!?~Ep67ETU%h6EA7mr zp}}XJMQ?@qlA9E#Msou+nrSW{H`V#aQ$=XFEd0F_C0F^f$=|T`t3P8#MP8v*5qX%A zd3gHt*+v@Y)}}_mgs{&T7gciRyRsri;enPX?9K-Kf4g$nCVRInz4dm3B*KG`vSdjJ zC`$NA>O)J1J`bXO8cC-jrPPC3=Niyck)&M}*K2|Sy0nZGe4#Zn)hQLMI)oA+WeI<+ ztJ@z%P=K9-I#4lA!J7T%`w#u#stOuyBDIVbSe!POA2-pA3+Qt=Xxauly7k*->Nc_ z74)tnqR(Nd?>ZTB6Vt^-d3ctoaGPy68rj8fe-%}U`W!zcCXO&HLkTQD>b;n>7)}I- z-B!W0elWbwKfiQ;J*dJe9x!=bo4hYIAQwMhx91Ys1M(4Bto*w}Ph3PO9j^P`lyG>5 zl-xjhE7%ciJTB`MjB)`TVbYw~`YFhtDwQwK)r_9P6-adtPfv9PnQa|3+0XYEl*e7) ze<++6KcWd=V?Gskc1#-k1)kY=BPALX3XpJ@rFhV4u>kcGh1hyUqzcFO%+=ju735!L zzh;(^{tk-4hl0z$xkbY-(o~<}prePE7*y>P*-;H?^mxVn9dD_82Df7c4LW&{wx!NQ za(2QnnlsUsduxy3$0v+blWg=<-?nATFK{%C)|d>uIxv&Nbe)u4bOL zif)(PL;B(fe!H9%F84lh#^1l|m*3BLZArBBx(J%F59hwLSSn?gjaQVF-&n7lpP#?I z^?uVoe1m~rqEqwH$0)YX?Cd#qq)J+V@ko+4l#i90!QI}(;qtf5R&)J&>G0%Ta|eEe z&B5+DZAWBK1p3P+E0u+62j{G}Kda0(j%j@~Xlb+X)bP|?zIW`egO5Oh+YP00Z@yKV zh^!$?A+vs}Oz}e=O2h^!fLWs7_!x(+IU8O!Ha)cXuxn`b`9+neW_)8^0)H*`pJ zm{bUeuoOd~w1S)!Em^PZ7e)967KTJ^-sS1A)Xv!Enw#IFpa8gdlwtw?TuS35C3Z2< zsAy>P4D?9g{CH7yUw6e4cQdd`FY$p+N)04btLGdQT!|pS^wYtCy-1|>;dXIhso~Fr z@uj7u*-3QZbaR+6LUMzUFyhIRSFhUJa7m+YT%9Ul)=--v(ymce9UV(MJIJR`Q-9NJ zBZ^Q-{*)G2S%LLBkWKz`n9uRxhL9qeB}y+VBlawhf${Rps91{hqv1N9$Tyc(7_Sej+8t|1>6O{PuVR(L6HF7QsdS)HpawYgX4$H z9)=G}-E-PU7iZ0?bVNj;EJQ8=4j!^PU}pm=h*xID_>@35eqN)QVYpcEQV<&ll3o(y z8qvwh!&13D!TG~lL$Fvt-IGhqN$ef&t1LWkXl?QOS8qkw+yabqQG5^v8xp6!nokzV zY5%1b`y(kWnVY}8g7?$-LAeVOafBo{Umm5p74Pn9cJpv;ViftXaX{I-GE4Zh$q`C6rN4tC3Ta4z?R<{ zDcYv_J>>wl zZh9xobaE)dLO;Y!cgbw_r-Tu)Ka^f68sBK7Qr7C}n1G`|xGKf9(G>2h|8Lq!WGOcUV} z;wmQBQY`syvg0bm_W^Y5V>@n(i!H$N8Z8y&2g@4klsu40>bG%`<1V0`W~A-X{%Z>7 zInKqU`@s4*Pm+qL8#lC1%7jyCJTk~X_}!AKzNWML2bbvy?oC`GJUgCkW@hr`e36^OaEj5%NlsZ&%y+#P5sWdwt^rnVrYq32 zq|D8gwI*j_MMXt6d7WnCvI3I-|B_>XmkE-%( z*MWi1fw-r!Vo643+Tdo&&?bejJB*(Pcq^wnQ@Y@D0bk0D%TQMP9>^}Mq96(;0T%CX z=Vs@?3@|u}`E}IDi_C?9qveXVo~kOMFOu2E(4R0iwKX@v=^f#hVXwZ(5isP;#XvtN zG$_NTtMDb}ma@U|v5f18L_HRw#dT&jaP+P@MOm+t0aTG5ETQ-!Em^+;S<;7k0aV=n zlO$nIE~iJwKiKu1h*ST~{?l%VQD5Olc!_{~*J@sw?oPf5&4D}QLFpXplS>Fj5$uCLBrZN9Jz(|QX<1fy zev)CCi-shqwpYkmlB>H1uQX!|$h&N{eD(^%^Yh;_`d%Gx%}61WeEw~51Ius^r)U(K zjsL(kY47=VY-w(MyifWKH$}`^r`{m_))rUtSLCd3euIYex~7G34Neij@~d;o?aXbx z-yfnuE+|D5IWd#S`48Tg3~b%l9jkoB=feyL2+-#5i<~!bSEI&_M|~ID z3Ptm6PusSv(PJfwYHDl(d1vbJ2`ZUPEwaI;U@0U&>64=C3jj0p{d14l#zybw&-sZf zbQoX!1JqsY9)}(QY}v(wBr$FcP~{1pL{Ns$&Mc}eV_%> zPc3Re)=bq5OPp+TvxrM&<+NLgRx0@8B+nnKyPblnjr{u?gwsce1!iNvg1j#5XJ>ilZ0lGd?<+vh&pW!b zM}^Ds!@Dw?2{h?~QQG8!b=)(wM-uWiVh0S)TyI=Nh5V2x5No z{C!LyELrT(+5JY(y>DhiWXgPZ4-P(VV%qf~hsTa=druVN_*XEA`U^;6*U7(WN^QNjo05`W)`}5WJ@KMO4+1ej0a6cJsQ?zDf$R8x=X-~f z*3}&y{hNtI5m8ZNmLk4qj2;`hhxjOeETBUC@UAaksX{)r;tvm@gi#0HqrClwdYdf0 zJOaO!00a!*ZxMU{`$thymBT9JPAR42`zo0@i6@)fMA%blz@o~U*P6=6#|8Txu0L3} zHLmz6^Ic;9`CJ(9Z<6oc-q#qY6>j2wq6-M5cVb+v?fyQ?xs>V^@2~;Y@6gpDTRxES ziyA5&5;ihD+*|omN#^;%rok%>+8j1Ee1xZZLHNXg3E$4SUX9+y!O9M!^f5eKgii>} z4$_mCRS-u(L_~fA)aPgTO|s-0A2%u7spbA!RMph+rZck%7#ZN=Q$Epl7ElKtj@Dxe zs;Y%8HXtqYo$_Ah{X&0s<4=e5YEE@$WNO$|O*Xs8YKvI6k0K-=1}^mhp`>H=FQ zEG)a{dWeBR`UK0n7QrokLg487!ZeF9qbc0gw)rBC{U_t*&PL8l^%+~OXp6y)afDxg zO_u2Y%LNFelk@N>uy*~h^kAjbq(JN;IKrDpNf0UdKJF)EUBksNO=FiS;Whx#7a(@K zZNw0Bz`e~7?MXw_^ref9Rq5Oo`a*DnqASIF*TOpi1>}rDp%CK!VG7}DVLBcY>vd69 z*q;x(cVfp!?xjF#D2s){*v|Mc6ZmuzTfPA^F2E5l3Mv9cN5=r4do`%NN#l+LP*M3n z93I}bfvb+8{Z3-omR^9_4P zV>!7S=GYyUZbDTve@j796v#RGJJVb2hStiRP|?nh^p*g%-T&H5g2R=J7PD5__+QVs z9ht|mr%3I1?px|=ox!Bj^qPV!_u=`gn!Up&RsR@@9%eSqjp;1WdFKR}#*a@dI<{QN zJSi0+Aup2W1C|ch>q7y{S-wK@7zJ5->py6LEsLdBzcs{S`9*jDpm0o$pWmZ)%}de@ zJGSyrq=O@!g7t|UlU9_Q>pFfgU2>l5o2eb1bczx%Dl29b?6?pvEhsc58zwSiS5O+U zw=Z^abHl=kn%HxF9324q!$cN+mjhcQfTdSgNQ6hn{lN7idwwxvxc`{!$x62HAdzfJ z=FS>3yP!Te03P|XL{GOqA^U@)KnRVRuZjF8nWBktVNSvH683_U!g`x=l#Y&I`A`~0 zI>iHGRnN$K^g&$IK4Q#t2ZQv6qcf7vS+bEmYq}*`DYb4MwXD5{U?0F=s|t`~emVbO zsk@V08pFrV?vN#0tQHGG*I(kCOq!y6^2b+kYA$Ko5gFVnIh5SVN^+;%)ucFc!gsWS)DrNw14}SCd9?HK7X;*mf;oj*0M^MDUu(~ z2KlErvLQf-4lS+AcR8-q%-vEZK*b|V>Rjj?6oL98wZUM~wPRYTFw0TFOt~C22QJbL zf20Kn{6j&V;$}XqP%eJ`OgZ&pLg|+N^7MJXZkkWya_5&1k15JL(hXlsflSV=V-65k zZ{?f69#R3b_Gd?D@;@`(cdxP{U>3OrR1RMGCo{w6Ja)qBzKM3h%*@RCP3|>A7B*HD zm#DBE*Zh$*-3l6F5{_i0d=+A0hu`3{)5iX$`S9CS&J@$;6oNpr#IWY1cg&PDx?7E& zhv&>cS+rQ(|4!0MSJz!@5-j~9BZ0A+cQ7TOVQLCi3`*3=*SOu?^xu&MOmX@*cFT=93qM`vs6>+cK?=!akj;9t;Fq0a++uIujd4)&DeBZFV zl2VpcUUPO@Rh;hL8d(UGZ zkPF;a(9bpT+2@r^UesmTfAX;b1=Y%dwVAvtlmTpR&<{B5;7cuO zs3@&m6#;ZgCKF^X%~=g8lLT6uigK!;FpuVd%bm{zx>!jL^;@rsx@P|w7^#^&Q1dCb zWbM#^lK!WZMZu{2s1)bR8uDU%~8b{`AvR zfv!Odbu~oA#9$hjkrYbbR9ty-!8dqPxeE6GmCc=fbJl5pR=KQ`3$0<|6`j*Q5HBR} z7k3(sTO41&BY9EjIb$9CE?~Fwqq{Jek};|9ye@R}ZTVG}&;8K(QO4^NX|#nR?@2Gg zNT#^Ds=hks|B1Tq8)f1&ood&O8z0L5hpn%Gsw(QXzJP$VgtUOPbazQg2oll_f^>IE zHzLyAA|)kV0@B@`m+o%n)?RzAIpbOx|gVH_C#W0rkoUU1K(QZ4DVKFzwx0ECk^HdZ#Kq*LRQ6&Up%?k4r5 z!c2mVe}Xn4_VT>0uB(xPKb2nb zaJ-pDLqk%e#koZ%hCoO<8*UFk3Qm$k7k8>zgYu_y;`)L<=)TJ?2`xO;h%059OB{At z)F`s=yOz=LD>;R4D9WRslUcQjxrp9Bd4qI(e=GSPY4m#;(J73#FMz&nFUFtWX64ti zSrSU$phK5@BVXl;ZE2Lsj@MMsZH?DWM&DkJAa<9lTIyL+KD5q=LGw32sl#6>Mr8l! zSa;M*S{e~+sDqktjTJVWK7aP7+1ew3qYmxQ2z>{p?R#sBdNHkZfxOYZVus&_-(Vy; zfAZ;Ry6XtVn20J0RGQs|6I`b1p^@tG-ufa!*y>tpqUC6DG#EC3*r#v-Z*NHAg-_hI zmjbJA`P5VZVD7(s(aoZ~G}mD@4{>YAqLta$v|&rJwn|iTFAYg?{Akj%q76?HO^S;- zF*0anDxNUZ*C)j*(4m^Gj6;PLUOgL%OCP{wX#Q$?KO7O_Db!Na(5^p>B9VO*f*(Fp ztRi1;+Ge+M-#T~c)65o@OB+TejfJnDcx)CioEBj4+z$df{UhAqvx(=4ykl%9O&&FM zLE0OEo`G7yh34Sc2KTh_vDaSPbq+i^Jq*VsK5qmhr(C5n zP-y82KvkZR!qxYMWFE8KIr%y1Jbk1(=L+^U1Q3{o?A;uU7)|Ao3KODFf7uro<{)MA z&r$Jj{F8QTTVjQ3kuwOGOCKcvy$P+mwtmTMv*bZ~CwU&I1nKVA#lj(;oLlH&bVRf6 z;KBq99Esyxl2UVLrJd~?*X!GBWt<^nHrN*r4-Wz)pE!~szix+|#J;VI-#G~0xYoVf zQ?{yTm%i$`ze^#qC{d+<`_(dfN4k^7Xpj9yrx>+f`GxJAstX9yL_5}A?*UKEF$z#1j_=6ISp(|WxpuJon{T#kEph%`sa80`o3k+7-C}v23dd{+uSTf zMfK^x`alDPO~Meg%uW>%t@tXG?hBr>cjQ87fA!y~8Cw{uZNv)4Ut9@KakDGjDfGZSoA}z5Q)k zrhaz(GdvBjvo&GVCcz@KW+T!f+@?m3N^EpXzD0Bmo^PN+|$sxTicuGhl^(ulh85onGF+> zxbG1_)N8iiLVp<^4i2SB2_(ogRpK$8##|Ge)rVD7g4`O?Dx_wxI1+HZZ5UHq-dqnr zL!$)1mH*HgC}TZZ8`g`$gq4uKxL7(U5BVcOz72kDZ9Vh-B%6Sf!C&x!wuAh5ma_5S!#{akSH#r{{sXH=90yuFnbc$=fBl{Qm& z64za#qBPXoYtNCw0o+;9xQ;5JSXac&!98hnyWW)Tw?;4%{+Q8p8x_49Dc4+CSKz6a$X{daC$Zs@Z8+ zIP4e3lD?ttqQ4L@t<;EWdbB9&wBP=-T{h%(;Ko50SHGDA2Rb$s2;crFd@!;0me&73 zOVFp!+H!X{olp?fMNNqqEa|!zOx7J0n-cxV}kDkX1@@DN*nRyW>{>?5pJ%G2IE+&_yr zA77m1;b;Feyi1<5#`%-#`a{`*wU%}w(avG0f(*QG$M=XYi=~C3tg$`A3p38!f6Qv; z_8oa_Thrtb%UikLu<;F=v${A{5un|eb&4x=?T<%TvN@{=g#bTiV->k5L<9tne-~@S zmJ|T{`h}4PiSdUTL}L93%mhPdylc3q+%fliC*Ry1IBZQh6VQ~ zRfhPV-@|{6M2VIswi!1&pXrbj!x86vNTAUI1%#&rUDJw-zI^+m;EA|oXveqVqwcAt z8<=wGHWouVqnHl@+$CgVDRRw|zp+E9G)t1_$9GhE%#wgACUm+)m&%u{TfTkGF0_Df zfI7A%BSXGu&AtwmZ-GAz5$G6e=HOsqLrNt>)#v9&SJR%m&4Xv~mTSYK7@+I?PA6iA z!-(u`0@EKn2KdhwT_58mhT6!wv7dyMmRxWm3VM2q1_s)e^Lu;63*`b((8ObKuovg( zx~a*FE{=|dUyJO8qs!%d<3};{QX6yYa?J#vhu8dwKV4rxuho|Ke@lZC9w7<%tF$;@ zL~c2(itChc&Bl>6%gGIe|CAy2KfH?8H2qt z`8oHN6KJelH1<4zoK2hZn|XDLiCmE)JbW4Y4i-{gI-34-KY_O(Y=+{wH?5pN={t5y z53CL43%|61UvD9hRs^t}MhKb8e z_8X9UVU6Fye);h$SF}Wz{Dy$r7_{A>{@Y7;fzpYUrC!ls?qg{fW(pfV!@(|C*O0Br za2)&Y9$T4Bz=)fJm8;*l+}`=+sjH{Phwk)y=4a0kU|RL{)C|ofU5z;s`V{2*<}c^N zfs7RR?G=m>aLguq2Lt+3_&`s0FW^sPt;CpV)PZrNBQ-t`%wApa_^re&{^Hr&-|#p7 z;b4gE{!Nx#4HY+Td}*xwf*KC@-(13~3C+v!;#Ejow?7uN^OBQpeYVid$gJ`_L}70F zIq{>OZ|={ZoJK5HyE<((=5%_7!gue7*a#wD)uem&(++o{#bx zvVH1YcRjCUrlamE)8jpK`)YwV^fFG>cw;?9E}x$f?bKn+c+Z`fNmz&#>g)i-DN7P_=@Cr++>h)suuXrEF1~?~Tbu2< zs+42ttC?-VLF_sOp!>DfDtvTJxV}F+O~7O(aZsZOGGJ~xN%PBQI}D+TeMjlNAL){9 zwAw@g?gscF(N$afB32eNMpmaS4;?jhXn$WVQ^)lZ;yVX*Ub8+NX3QDcwrRWq3)I}xKZzkV~ZZ$Gi9?TxTr z%7VIPTf3O0C6+THuqifb#+i|RW zbvzUa!})#h?YNv8tsPKAPLi8U8j5bZ`J2&COTBBY0|Nn@e7|h_(D5MPvT_N%Vdu|ihnkJ|#{VS* z%fi(JHw>8|1-Br@<@9aKhMgO8lYQ00->uyv7L5szFM!c!`0dbOvgwv<)Yx4|9IFgkm!4j2 zeKloBYb###@fB^GP2R=DGYNM&1dG<#*4d7!t|PAwCpU9XFrxjbY_?Gio1eK8P-jk- zb_UK|&kzts>fZ0Y)0p$9nhg44g%We|OH!!z*bd@1Kp;PrK1FWsdDM=$7*TQt#V`Utbw>ws#> z72LNnsr{%~G=g(jYFvJ3#mV%axyY6EPHylwc;w=SJ+oK0XD_rGd9-y+%%_jc9lcUkssxbxz^>jJj}R*pUidwO2}OChYQbw=SDArs zt|Zi&Qm=jIwVabLYAvVt->drL^EDy1)32-;&mqh9bs26Czr+be&SIZl;^!4&FRGcE zbfaO8;6?f3sBvT+lM$^h-D_W#J(;B2)@jp04VCZF?0pkvj5T9%AP`U$oZDw3JnPt4 z?|2Rg=}(o`i?+ zRm;SG->GX}w6A!-s7QuI)C2ZC4kgd^JxywWVP58MhYO0@OZ@-zjx| z{wS&KpmeUoA+WhP4XHC2ZE3`UJdSbN{Ki9w<=-oek@U8Vrhu(_#>%}(o(yNi%rQni z+sn6pisIE86nJuHfB3O3@}oAu7{Nbl;s0&=UsnjzDBanlDY1@%`A&--~3 zxIW%roa)y!L@XyKbH3d80=={~@`zw`W`)MmRrZhNW2MdEbmK- z9v;42OF$h7_jmFfRKwvRHy(U`Vie%2-J5}IGy&JVR1Yis9-LK!VX#_uGvY6F#o|A~ z$2Ovx;#U%Cz6dQWq+qOk#rBj@)Pj?I4pK4^0w#9kux4a5;E;foxUapDC89?k`I%@d zOYoSTkN>VquV&=Gqmc1q>ya6=XviVN;*)tVRWm7I92ZLdn`vKZzK?C^j%>kx@%{YH zu{IS$cNcbiHSj6;Kd(raFS6rHn98-|OPk~5gfTxB{kN3O_{tY8K1fuIDYFxEHQDQy z8>?52ll_}*-Q1}I<_n9VgKzHJ+s8EJ2yyp)9%H}!n+$$rE>d>0j6qbL-vX(yDw-%T z|1*MpF6m)PGL21q6ts%S$s8@DNl4u{>2c$nw>6N3W7- zcWh`pe#W{zZ!vY6%d zg(jUv0IPtH3#bB>fig1uv1Y-TPNSnVB^G?IG4vl}haxA(B9=k{waWO#(dP0lCK4x3p|)Y= zf6u#OP3}GOB|B_u>$1MHiK?~y@43j`^P9=vT;u}$^z@93FX8%E9JOnYIFm?bdY)2i z@K~LR_Te37PhBKy{eWy{l@!bbYo`1KVXHIp>MaY}|F>Y$@0 z12r&csly7@1KT6X65==v*TW;0`-`c^^zYw)H5f*_bfAmmDeYQV|E{WffIePNE!sUD z3EjV*FeZ(biV?A$)Y4hl<*m;hOtMGl%yob_@epGGJuePTMVu;of_QbJsws& zX|5C}ggIY`%}*+(?m%R-)ay z1+V1EcQK!>n6yCV7ok+%IVawSA=Q%5d&5vtJ+hN1*MJ`?=9-%3w)(?|k5>=N%U@lp z6yyuo=|PJC3jb+TG^7Qsk5EUxsr;umEuHIg1YSkuvu?i)Qyik-PHunfJiR3`$xz%| zzg#SNL!k*6q~GAXH}G(UguGYhuTjKnGgNNYTCSPh>gvqncJzGC57Qey(0=ER_TH>B zO-DjS%~dKy$SE%^^;Ty>1f1%7Q(@Pd?Cgl4H8hOqyWKMHiLtTWI_ARPznO%6rd*c1 zPnjQ1${@=FIZJjtd)4P?6COXiIlva>@cudjy0Na0?|dtN+ww41W@2Iz@?(7bd&@)7 zBLy9sp|SDVX3N##I#a-aJo3}r*`p<90vVbEj{tgb*T&Hdu~3R^<3>5J_Tl>Tldnh; zO4XX%uj(qLO$#_^X4aR61A6dK} z>7s#!h4sE}c`()2C#%b$iJfr-a)8G9c`*3a*4F#mpF`;l`op_Dpg-m#_v1s+A3wk3 z+4__kbL5j#@6%m9BtYy~K6QOO)q9|NE!13DY2j>rVB>SQHnDai@CLjm>*YB~@6#=; zRy%wSMrVD=Fwnv~snZP_>BBq4<=9umWMnEfuiVQ&S6h7RG;Y5BOQek4+2C_QQ84DV zcNMCf;dMi^bbk;@#Ep%O#m3H-oUHk911y9a7POfzD`UHQUY(xU1J|CRN3*)L!m27B z5nKo)X?wfDdH-f-H~evF=n)fQJuyyvyL`{B>vNy+*!4(udG5H{(bLZS>Sj^PW{9!B zZlN?CM!3oIoDee3&Gm3+Dtte=(%$mePO9hn;6@UCOWJb3 zAk`&=;Jd!QE_`>u-JvTaA11&C0@ewBR;lE=8Fkz{=Y)9IU|5KVwkL7QfVGuX&Iejl z1UQKMt$SZz@0EbAwl*l11^OqX~C^7Mj}pWu9p`{f{? zt<9*oQj$UUQX2lT6IWmvr+;fq2?>yjkR1!a6fFpy^m2w|P%kYAep z=_|QQ4+obqD>1^4GYX*t7g13@j{`POj6+~Le0N5Z9{Psk=VpF_5ow0&_VQcgK=-}1 z=%(zD9B^Voz$Ak8^#zBH+q1Gg5R&g@VOu45iu_lqX?QcULRoi#2GobSkcDqEh<}OvD!^6c3wQbVHfIQyx`Rs1*QedxH>)l^~ z&h!9d*yc!>kZkL!bi{@JRMa>bocd$D%W?pJ!kCqI?)fTQj1;USHM z4rF*@2G~#}Meu0q>G^;qcYPcuNneac-qU{X3hD5JZxn!1r{fZ2Epc(z;abx!(1BwJ z0le?exYwtlNXH#0M{Y{Sgy20jroWk!+FP+RXue@QKm^H+Gp@gGks`g(>Nlw%wYkX2 z@KD*wA8{CL2>}x({LXh*{7a7yw=IwNX&27I!e$a~*VRNIK+p(-ReERdfQh01aXI>t zmya(#HPy0m`bvPLJV(ButoX;(=Hr8cWDZihT~pie!kEX}#=zjg!SA^maJ%U-S1{MG zjY301XEZb%XN5BaeZ2Si%d$>gzm1Fmn{te%t5F|tQc!HigIs(({PY-&On5tNy!Z_i9hBasgt%iG@_#vEgR0<xkw$KsUI=gXs+>;t$wDS1*^Frlgb=9KxWw7w5nG+EgUT93d`l&f@$K$a()0Ijgo*6 z;`N}TB6*yJ8^^i|9Q0s!Jst&L4y6jBh)0`#SPu5NYv0=1!g$yl0;%6~zj-_af8HW| zy~U#&wEBu_tlA629Ry*unA^8cb-dcmpPgO>yVUde#SzDR-YrI;X7s58WqcxJ?*jk@ z{^dC`ay^tAyA_&@d?n_eGdJJ5e0e+V3EQU%+ABHmxl8c zcD_-Wbg|_pzbv&X9Xyy1!vk26_wIR}nS_H88YCbfAVF27l6SsOFKq=SK%)+RQ$CF) zrG{f{Bd9SddQSwQ>4J5#oGVx5=R-!zZBGq4srw=GLu%g&U)tcz%nXJQ-$h$+SL46k zu0n;a(dNL#^=&GLp#w(Is?+kYg;I;JES~((0A0ve081E_Yi!EC;Sov2NfI@{&2{Epz<4HSDpF*>5z|u^&GuVUmzQ|14nVv;)*b zzTh;DVhL*N206OmKh$Y(lxO99*dg*^%SkM5aDvX2`9w#MW7x0eEX@|Ha-}G%Yhq#% zuGrU0Lg7jVoOq&80jDz8q^w`ug33 zjWA9&94WL=46cbOfu5QvRI&gK?F?3=bJ5h_PZU6z-(^vxd$ujnP2J5!3nt#(-|usI zh~1y(1;Q7M#U#fV4e#btOFC&r`iNw9nwuB5@Cs48)Mz%H)+No;A*=_Vg55 zx&17+ygbPM;}>vswce4N+aVq)X~a=NK>3OoM@3VKyyddGQ`FDjOofhB_O$Z$62Poz zYC&GkkKX=m%jfwi$9AbwL84j3lRk}ociu1F3qwRuWO}kAT>2AXSPWdvGtKlrK);FS z!SmrUs&Es}V0$}0nEF(}(u7%>viVub?J;2n$7zF_yzuK)|U_?-`m9GWKRkuoX7 zr_D!WVWA9K&0|o?>?_aLm`qr*eqw)iElPWXZ(#K8tR?HJAP}b-COAm>AG^<=#q~dY zb}>zfUhDcxQ$90aHt20VybBg(HF*sO%9A*0Gt0&;7 zyFE*VE=5bTNsP30f6gVuJ&u7a)6!+zR4?@;A}u4MC2Pbr$WqbRcpX$4o^14qYOH6h z=2+k-oL`xm&Q=wTNZN!r}r{=HRNONO4Ld%aR2PN=vFT{U12nbZUa26S}% zog5#Bh=__dinQ8TG+^W4B&Gn)pL0V~c^&AK>iWkiPNs7cg%!*VJ@%#a~Wr%e47yZqJw2k!9~-pAF2C z#r)u4@3b`M==%ER`1!ytrGp%lWnjX4{;(967n)gnr+Z!k-ozJ?56TUE)-*h{w9MRy z5KAr(Bjw7)Oq!@Q$9kEt@DL8H(Dj{ai?HqO0sE6@Gr6mQ{FZZ$Os`y$jaISP?UB2` zBAOMR`q?-#AO~tTJxauxAMZdN&>KoThbkA}OwN4{IzzMKN#J|Ruml9;5ONv{5W9|x z`}=Earx%+eX^IE?yFXi+D-}){mQPo^MT+C%|Clge-~lb~8}7#o*?E?~0PQ?*lgQ4+ z>X~uaT&U#-d$m5O3>B78w9iUW84MXa1qD5O<^F{g`SCu#OD*x>cTrX$)^blr2N@kpFT};a}ct%oON462Q!UIL%`;EKGVglxNodG3J=IRD}$it3b$bq(iBhV$}WCcxKF< z6dVaTA)w;4`oUmDk*@@SMZZGb=;U>^c~MRjm}r5bP?P}ioP-bL6K0&BUkE>|A{bc-gV(=?Fa2mKR!_eQQ3rczA-UMKF-( zNJs=3iIf|fJ9|J@(jVa{T)6bGV-Cq2`Rwwl03>c(jK4 zk^P~A7@VMCsGhCubcqF?;3DEv*e4P8%mZ_V`!mP(-X36&0nfYZ>+7|(HPA3Fy?h^x{1T&&)DfM&pl@bmM&lQK8E{-y~V@bh>ncB#3(L(-epoQD=S;7 z)39R82hne^KXFwY9~)!RaMv_la*Ql_O2xEtC{hRw`@oUKei9SrXCOJe9g9_c_MWm`ET( zq5{Bz&E@#0`2*Q{pt18|MtrjUsVW*oTv8GNzBgG@6NU_cSbTJ&#%yLBn!pYgL@^?w z*!1`vTRt5oH7~Cm5Iiu{e}isQd&O$uQ($?*Hd=up`#?RXZ+kzlZDds0x8M6tAv!eb zubp9EJS$=mal*wV0t10!29m6$X|lP|O&jy>tiRa{h)QWZ zUalD$jg5_^q8Ic*fhot-_PMwrdT?b!)%U%^y@PE6BGSv4*nNq-QPqs%`KWEmZixy- zu;)bh2j5jh;-hX98-Ci!-cRg8sZ+x!Tb0ltiUP6eDLnl*#P|FpBr^=}WSnm4w14if zQR)r_9qBS^@twKkVDl!1TadVT7l}LchiQlnR^j6==swDSrkiWB-grOg!D<(p{-wuE z0~qt@x*A8AX4-t5V9w1oaSJ}6ybk;XeipM*8m7(^zTRb5++`yQaw7O&gS2Y@NIJd7 z8?z?;o9)q23|b4>n?eonO$a2o&jS=PyFY2YShoYH-+JLkpWU5-B0Y`%{58@$6KmC# zS55wP+JjXQ+V+THQhpW2ma&b1VkA> zrYs^!Flb-%Bc}`;qt{DSj>WErna=r%W*r>mmq`sl?VX*f>a?})VPex`Zf~>>dtv=S z{dK*~vI2ms0+Hw>LO_~^^YUdM-t)~((;aIahxtCS2K=B;#)mR;@&z9X!4cn|bBF2; z5u?{sd6qHIiWf{=I9A7OU&9(Gn&q{$^c$9MRV6zVMBVa)&~P6d93FsHvC48$M)>jZ zF>|vjDJgoQ)GtDFCGuW+!+gQ5EL@X6?LIp3^hc65OX9tD5mcMYJBBQ|oEOaq z;xJ%aYoZ!;Jl0PB+%Z5pI;f~A^Srzc93W)&F9}}KzX)~Ge7{ga-;qoJrWFTYWRHH2 zpJ=LlmyUxym_u1@i;2_XB+*Ng+G?ujsF0`G*|Y!@uueoKp;ZP9BL$PAD((6#dOo`g zH(F|Am8a?q7sDcl!y*c1Zq@H7Rhbwk<{JUg%ckXg<t zRJgbowKcUNz*}m4>#Cy&Kqm{V`gu1L2y+}%y0089+#PuuGiydfXEo%LYV@W9#ySdozfI_{_p z;ZrlOfr+D#2}SBW=mg4XC5_O{ezmOWJsd27Tzs^@D*@pi=5z5cZ?j?)?c@u@SgU6- zB=VFl1swEE+)vI-NjWE$`UjYiP=R1f*k=Z8<3LS+D4oxMW$XSfVd`a;QsKv2^zL8; zd6HW%ul;Hri3aXlOogiQG=8s>y963|SVc|A-)eqm#R9F%;jyIJ4>u9wt6ghIU0sr( zUrWz3%$V{u`b+upj>BDt3?Jt^3~eTx$J5|?K1~IZ+oAH9w$YO&;~QX=P*_;b+;Uah zQc&5rqrsJe0yz#XR4hN{F7PV#PAUHGWHmXq=7QOu#eWNZ2vGL*?@ z%=k;2TJyt6M{vtWxu`e~9B+Nwt+ctw4B}fIvHkOkFfW$@X-7FxJr_{U+x*+L4sQ9X zeI1~Y?edpI+SV{!M^O#qM~r27JFfKiWm{O_DQ-wVJ~sQ5;;6%)*Xew&vFD|$?ch2*n7F|G~ycI4<+}f^9OO8PUPxB3+1Ld5& z_16V0nq?WnkRdT6$AlB#Jbz4C*Dx~mqn%#@Zv=OeuN+{X{-Y^>$^y-OVR}p7x4*&r z@5)IW3W{pDM2Tj-`*E>uSz1bxVdacy`7?;PA(DTOc4EqFECu-}zBKvZb|E1?OE$vL zxR1dI9-UaiglY1*Nok65QIYs(_05Y>bp#UHo>-9F|4HJr8SC8U(O%q}uNp7`U(juG zo%E`Ka<8Ajq$~ujxw>px0W7tK4cT0kRies!U{LP8g2EyvjsLw4h}q^7pf1N$00&7$ z51!liYxvaT!eC%&{G!V)O}>GJyGUE}*PNAgjh4_n=`ZQX8I-6s`CvH2`EAD+VsKvC zGv`as0k<1aheut<%*(+@FMj_G$mM{%8|Wt9IZVQjB|^PvdpHQZp1JD4IpNv878(mJ zj|usKZ%Ae&uF@Z2I9k_8-T(7iBhb)XyKKsN-ueXNdG3cF+DyXaZ25rwQlgrp#KbkZ z0O(XF$rDCshBI6;;%{_B2n z*R9W%hK>lS6$q0p`iLSfnjPYp46XYKE?c+kWq6+PVQxZObLq}KVnP1j@8KwGIqmC# z)2K=G#9i+tL6Jj=X%NoWv>G^3%yZHhAD3?O%QhP!6-jLV`qqcUZ|S_mC?|APTd~`M z`Tvk9Sy8dWa!(xu*P}E`G%4f1rGQ7O9A`hWZHv(Dhr3})kz*K);4Wx{R2nDBl`?=I zG(zB*rEbWTLX6IjSvvM}8%wNeE7NK=|8>E>5!$#4D4jgD13OBE@uGjo zq3!qh@F+-;iOKjn4cI3GqQ&p4tP!-xsFj)yCqICK!dK2zrUKQX*W7{MIBVj^GJGt# z2n5AoxorY|G`U^Z`Ik`^e9~mt_#KY`U4@O4@sC8Wcpcmh$XH-~m_jo+ejxhA^ra?d zr7dXIVlitM2PdIn{(xQ2bI*}ybv4!q`&%Pc{~(>b&#@iQm9{W!i44X7%5IhtlYB%^ zPHtKjJyUhnq*4sc$$2Ol$l3T9**E)5Ny}__X8P-e^ZS0? z_9FGT#CvuLQMxo$=%OMl09vp+d}+%yD5x%+%hCLm0>`V;tZwmH23!ik94>zYqQ;yd z?fI0_i+Hc0=Cl1n&!H7ydDVUE&ey0+wnWJ^)q7a9pIDb3P)~?yEDP525u;_CAOrC@ z*@7un4F>A)_W@&{i-b+sDo<@{#(nM{d{X(Yvpj#`aS3;u02ih%so+M~YQng8RrOKD z?`WwAThH7}z+V`c;+j*jOC;YnQ$XJ3J~;BhK>%5kW7a-_^ew}A27WBImEw13hP%b^ zXO7Ad9*$uxkXv8N_SscHD7M`ERgUxWZjS@^4aedph$wp?n;$IFBf9fA}cl9sfXnPDfUSsm2Af<>wic@aj5-Y{UP zd&(|ZHiaBO$`rH|#_ zq~J1KUhc)i2im$e>cHo@!RJBrSyN2%XV89AUOEz)rb zbej;PH+baHgn)I!w49ef!_Bjqj6;Bpk-ny;=oKU=*nJI48Nfwuo^Q29E7WT}qf_yT zOe)lQ8^YWKni*(~haT`?Vf|T9Jr0)XbuUFrB;Uls!$ADoB0?fMS;9;Ou^<zZEUdt+k9E(I>khfFWa{Wgw@EXMT}yvi=Mq_-Zf|Z}p;IpQTV` z$3%ntDr$keVWD*5hXdgB!v6tzFNA;>-Hey0qu^B+pz*vNFFADoBuEe$1mb-~*u7}E zIBoo1=`3kVA}qvf;T2Mk8Rkg2Ksp&KuMRH0fWS23Q)q|H0wlaRaLU|I*iwsoa)QjO zqpPrzS)e(ct6=|@e~)ouDn8?zC7`>_^M(QWOVh$D2RI1aqr_YBzW)4!V+> z2C+IN2|xz?e<;VMII;LaD?HYfngY@DliGTky7yqe05n{`H|iJSZXHg-w31alFu*wQ za1vVXX0-tETWFlSobAfe(~^>!sj#{{Uan4pJ*)>f095qeJ!UMO zKb@aC@)kvvho6c zYIs=B@vhq7(s{4D*^ee&btSjuJhe3R%@EJboDw8RmF7o4Z>zhe@)Xi5QPQn*oV2ci z@$IxU|Iw75qV`?u!|38H009k7+0W}kl-DT0{8A>t*kAmpym0Qf8it47v|(SjE_M$b zq{OeP46~~mll>Ie+wZwcQ<>*=K*1v~F*%W#bm2z40&H=INUl_s%&E%@6^odBy#Ctj zO+A0hhE~>!;o8YEa?ovqm!O!AD1#l-RSAIb}7Dts$x>s z_wqT=mr(+ip*`DeSq5M`0)2JN^MQ8UKR8;u$uZt_8qmb!Kkn-#rfnb8@28!u7_lT) zhQ;0dn0htk(Ks?Y$6C*8gH0DFDS-OyS9AX%;;}yeyi_>eF^u|Pd(;C_xPUP|X-C@p z);az$JUMRofPO`)Jm8a~Qlqrf#=l}G$eD|@Q7Z|T%#o7pl8;z&5T-am>(XcWlVd0u zbkj&dg3T96{UU7IGHPVxuKmBc06m{u?Na|{vL`X*nEBI0VXnWO7wg>hFo$~d!VgMT z4w5Z#m>@%!U|}C=&%5l1Ju;Ud3XGlWiE_AS^KELtQ)+G>(81B1rsVP`f)_IgKi;O} zh2|wNryfU%yvWeyzbP<>j;>!Nx^x&BR?a2w*x(#4wr`@?w!Wtx+p>L}?*7{+K&HTr zs^8udS<@-#$mxu6nWc%h`i7SgyQ>ZAEY(GccIAJNZORGF~yE+R74nHJ}X&QDV zD4D7@B|1O~8l;au-xmx626b%kDK%Ox5T)CFI)AR)OY4oqA^DOjI8Mn?ZHnRYU%P}p zsQua&(`}@v${)3w1m*|ydsNWl(-^v*A?Wmn-_SH9B5a_0vD)AQ09B(Ci!y`X&O@Yg zABGx_PL44i+W8IB(gaY%ycK~wiNk#}vK{(Lz;S(V8(T}GdOK^@omZGLUGM8an8mJ= zQYEavSec$i<%xp1q`a;-((x?R{!Icd;Z7;`=|S`H0x8(KEY|+!i|GKOgmdQw0-{$J zrwM{CM{}eP4++NO?f-?$JYjt8p|KI&Fzv z>g;Ofn&vwLV4~8kJ?cxe=b*YKkRwY+Sk*%JT76q#g-Twim^;lY}&LU!v0*op$^!@tr6~G5SeH?x&{^!+;w{qbLe%H(b8e(SxKL+r!JZG;j^Q=irjpi zGL0jjmE=V!*t#Oe@$m}+P&M}R912o+cONsTnYpLsSJydPo4Ccj#?sIRT9Mbdv*dCS zJvO?x$^R2=EYQTo*H+O4_M;{RFJ)z2^uB$$>enBK7JxoK^+gv#q*z!&4$*yX<Do3=Yp3dA6Ph4M=`F&tT{(V%;(x-B9O?9R1 zO;Y{wUH*frh-kqbq^8E?O3y0ml7)*XiG!|ohOygXR~W^mKahF)kIb#4q6)MX%FFa{ zn;FA2l&jdvIfiRC+Sk42EVWRCKemX9~JJ=nC?) zJDsm4cs3u+z?HEiGN@lq$gdv5>v?-e$mGPS3B|^$iK9Bv(Us(Wpxqqu6wMn!k)>m+ zaCZ7#1AMk5B)hzv4%lnv(&A$%P>_>|weEM9M%8mBeuA=osb1Z(a|{Lx93(`1^xMhK z-`U-td4WLY+jOgUx4Mqlq5mDN@;x$I^fO`=E8}eg`pz@ zGDek+n1g9TikjsLIZ787Rs65e5&Wekue&#c@~RepCsbmj=p27N>P{b=gS%fq@PGaK z4I>Zb-Rd=Sv8sitytl>#yIM1!zkxa^-=s}~TJUw|@~^O_As_F%X!PJqY^{L$6g5D0 zfC17%x0OyC?535@hN5EC-A$S$Ai1N0PMDR>A4G8xMyl3bUaz$20u_@US$7XT7l9XA zN;?Y!HCNl`@z`DN+SI}H(mCGiPEb_6JPR+aYMeExeA070aUXtF1)|NA zFbDm+uk7%FIAAwU;8t!w_y zRkaV@Yd2Bzban^4GO0SALwjCe7MDU~y|NxD-#XBCd(Aa*Z!EM2(RAc$P#GCjcE^zi!fQ}y#HoV9k^;ai(ySO_YMp;D;L z-wzo!RnV3=j5YSFqyJVWFGc@1AV&u!FP9FOv*dcp80!Jh^#%_>Wz~e$B4poW?hdQC zzDiN7(8p2cjELln%xIT>pkz&XZ>$A6dgrqzg4%<*Ap?cS6gy&bH4ittpkN&fccnW{ zMgaNt-TaO#bBQV=mTc%0kx7bN&;N}rs>mASBv}EefpNtX*B7wODhPblC+5OEf7Mjn z@Cql%)%{}OWz8OA#LMriyURDJOzI9A`*KZdwJGDiBv521nWIaF)3%XUuCeHOdS9rn zCqp!QVSiyzNq?Zpi*6@5)NG5dcb&cZgqTJmcsW{n%4l*W^ueM2FXG-ZtgGnj7Tq9% zBBe-3BS?2QC{ohh-AH$b2uPQ7DcwkSiGc8SB?%$!;}$vC4Bo0byF#!bRc|)UdYPt zv2<0}NVUnYM}`;@U}kKt)t2080};IR(~ZcKUF+$~0?$MYLZ_wT(>t5yylbYp-M!-p zIB3(VXVxU%M&^zd1|L?4(>+Ix^t>-2##nxhLUvP4T#Xt;V)g3V9FFeOP1*yvx%J&3 z5?R{C*4Gv`I^F;>0ptVh(E5^`WovXDeF);xn?e-pwELyjXDV>Z#YV>>gsoP6c@27T zPrS4jwmB+V3|&I`S-@x~L+x!J4lyrb1Zc-ieI-A9eQD;bVs{f7+41}Bz43b_`v_03 zgBO`!DnvAU&nTGMY}@otM_k*Q+!`i5tv$E}>i}H2$F3Nt_LhF3dGkco6dPv)TGA9- zxCqmZ?8aPMPtdyAL1ETG1p__*aKcjQYGs)O#iDW7Aa}q;l$IWhh>!+j95|Qm5iWjD zpfD74jxn9n2>0y2=uMN9i?^Y_;t$n~iimW$GeL{`%%MINl+#q$Z^GT7A97Uie&R*y zta$U1Ce9E0@i+37J-BDBI5(?uwv#P300$K=UsR9;_4 zk^M^|Fz51R&yp)pehY;HI&-c*q@j8d_4GMH=I^Kl&b>MEP3s z4jw*Kk~Rp*!;XM62_+UY>)pDC$lG!-WSn1BQQP>W`#7xlu5f2l=@c>f@zTRu!n(IWX?qxVpo9uqimn8PZT ztdBtJC_nB=$ykjyGTI3FXz&QB5BIvb z8CWLv7_9HnY#d)z{D|Ve)qVC1eA$=L@%xg7|nfx`|Lm;_JIVxBm}9 z06W!PqKE+3jZMKcJW0mKX%=mv&Fv~G)LpW73+XB6wSvhuvyW>tQkA+c4}E4xkQ5kO z93Mu8__bX?%O{=xh#onBg*6Mv2b}PsL3Dln>uXre4-<{1vS}omc#u$$^7vvya*FQn z&C_w3^*Bk>TCP@SO$RS?w{uuoi*b44Z1S$N>cLmnDVRJa6r>+^OpK<^0kSG@RNta_lCX z`l%>FV1)`J70ncevjzfSpTwT}y{u_Y&Fd0-VoVx0J~&-G0diP?Dd{}-{NJHX2&%Ry zD9YNO_AjeT8#rcmbLrmi-N6yK-l|R4#W;C3%xnk9Khar-7%9D-*QdJPGjblyzp_u- zB+GaR2VP!7$8gR(Y5xn52J$WPNC#PvK+Mm`-S>qWJq!5Kd^a<@w(+ltBETx|vuh!5 zzV6pihB+#Ma^|$m)w0YT(|E1UOpVD91Z`t`_Mr@$Rm@Y5nei+Dy zMf6@8_M&U~IUk4Jis0J*I~Iy|>)L-F)Tro=n11TqjkW}}yr~Qc_^73iqUw(lwvF>> zd9-wVCYF|oSL6`iF^oOrf1-4AC;tPbTVFYm65;hsil&?&(Ao|D&~AScGOMxkkJm%0 z$CNmVnME51U-mfrqj2eE&3n2E%qZFJtWr^g<1Ugfk6lFtE zi!WBquonIM+oL|LjCrNDOsv^Ln&cXNdRtDF?29Yl^7I(JA0+e)=rjP?S(?hIoJ+qE zi)$XRL4P0R@s&pB*$Qg+F+dE7`|&~N|9?C058k4DXXD-WyliBgZostJ$VM#55_-G#a~KR{q@ejbJK;T8OI^~2lS zRaDZi2zK(8gPX_85(6IgC-wuO*Bhy)>8EkVH1-*66O(eY@4$@f-NTQcqO>eHWvK$R z9V}SoCR>-N|3+V*o#=Tqz3R&O*iBJW8;nkT)p;_-FP_fTKR$kc?#(<>k$fTr_&{e`i)DO1x%EV{7wza=b_&m zx0;n>ceh^SSC|4Hy{}a{(bH5E zbu^UJY}3el+LnIReLY6g>MoTLSvaR1tKg^O@c7WHq}wX=g&cpUuuO-iY;u=K^C2v) z`3b~#znL(Y$~88oX8Mqe@IGH@p80q+zk~?1M0l|5Dbp*dh4pOJJXa8q7*rWzwOf-e!><(@G_?nndBqcL;_wm)#qC8j`Ct$E6kZ5Go6ZKzw4kRVe1 zEbF+}mXF80Fwa_6=es{!Qy*btye_RkUb)5FZ@ZF}UeQO9aOC1dVbx>`I}bQI&XHPi z@_P+|XA=8T&HNv6kw`C6RY=ZE(F>^cvrptmB_Av;O^uHS>*G!at15g~)mBDSma|m2YfJ4hbUQZQPo&t50Bjma>y&fC> z!X@W6&B|D{nFg2!m|df`S`(z^#bt1b?+NiRF&F1c_V472O);EvJk*&hvp>K~VV!Ebx5=zjwARR-TREfY4TGCL`coq$q5qB=uHzX?}P?MO%4+;ics4J8kXW zsyC6}M1Torep8iN*(C3&7Nj>5CYG-}1SGPU`r3!>mP9SF!2o*uc~OC)3DmZ2h}1VJ zw|T7f8kxKLI8>+Xa7W=JRU7h!IVt=#9%ET2m$2|~`LNF22oQ3|b%!xUg6;wp)Ga{P{8iC1qjWg_WbuqwU#oY5xFx4Xf}^~$ zG9$Kpsy}U!G|nhPFeeHYs5bE3yb^!sa}muah=f7E2}llJ{p45jONuO+J08W3A-{hj z45HN3EU)Fu)?rf0C6*as#DOFL8Pr@;Kh`6}8 zkPbfwjIEy>tR_tAm;hV*Kg6!}~bNP4_X2QL4nw zZFcsFB=j9rOD#w|nJGy$uTtHzCC|mB_GD#R(Z2pQR9sVB@0X*lMU(rp%g^*F}+Li{a3(8G;bUN6L-l`JpF@$a#{NYrBt+k8!gHUgyn42=BA!sT+E>rtbH2b zc8auEHk3V>yCL4OH(*MXR28Uo*+f6^9IX4Aog3KjOFlZ=ugRNUvuKr03kt9wUAoiF3L<!%IcRJ)5nP^TrQS(YiBFQUi3s6w} z6e|JRsYVyG@Dn#PNQripwPo|@&<>ngtX z@VPE8ttnJ20B6DSK*{4fl#den3v1?;H7>~RS1 zzea*Ni3?tDR~uPbS*;HvYgN;E5fA_FGg#5IH+%nG=QXxG4upUZu%_LZy)7@+8IcwP z=6xktzuSMzG->gbeX5hb&{A4@fP$!)H|l=G-?S zt;>D!xJ5CxOd3jTKhG~y+U|je;l7-bl79N%h@);BIxQp#E>|~CP11Ni;GS7jTPS7Q zIS4V@=tAHja&of&0tHUD%*3!v)^P6NKt2s)z3ap4$julYW6=Z!Y74An(G*`kw+@`&PXl?$i2|~*)}2BY7jyykpCP+{ ziVCUvxqWWh7Oco6fK)pKr5LJeCAu8+l{=_+a}jvp;HcJ1A%d|VF@}Vmb(ZD@-%&F} z|Mr#e;=hL4~d@|DTs<+5F`K!diFXi{AV9C-*KCKN-K#ICyv zG`w)or*}fLjA+)lPeFZ7>S7@!Whr8A=VcYAFs-hmL5^MhRpes~x%tp8oKV^5%dhXt zCNaq+Fc7f#rl)zUs*u@}(>HPhN4wD#GWrcOSO4WBQ7|_CozJQa;IZ=B5}v#B10_>^ zTwEtOA!D~-wXwGeFb3_uidq-jQ;fEu-j~%Jji>x@z5{ww)4Xgi@DX$tfH}p{>spQ{ zZD6SbgRe~NpDJJn)5OBE!LeV1oF|i;TbN2Nq3oinD4Zs=H0}`rL5Ti-Oc>2etV#nqgK)k-x!sJ(Hz^g{^g{;EbHI6Z*nBEI62xF-k#2 zLp&y1C$BL&g^+{hRR@!YN2!mH?ZEY@eZADof;0E+Ug8!PO2^*bejKt$-iL*py`~t4 z0fPtM&g~@PikZ4GQu$E1aiD<`XZ-Gf2%aF%@Bg~&lvU$vB>xa9NSGf>x3u9y1O#>I zIxY1HgXj6kZiWi1b;gza;e~|s%(Shr&g&W* zHOVdDd}nr$QT%hecw)!_W~k_uhO}5&D^AtdN|7M>&T0iVZCa2_Y!FNOjKz{jJTf7) zs4)w(tnMgqnpK%Fry{cB`Gc_du*6N)gZ82vhrf*c9Vt9Fr zgt+U(Q?He~4HVhgK@8Qe&~VH_&gCkqX;|SK$-# z>E?hgi}6kBMsAny4~$gNR%K62sF3E9|CbIEfr=mHEnWVHBGP-1FStI)YR$JJAtHWBaMIJTn|7=-oWrI06u4KW+4;t8h zyIh;Y0Y)}1)DkgTrK3w}W$w+#4#^}ay#q3ol<8740^jYkUzIoWCjgnoBmUVxB_P+d zv1!c>`rljtG`1Nc>`-a` znh+B4;JWeo4#lyVzVb>&M%wfk4tyy8fdg9NZv4CMWq5BN;OMASyj>8yP1xA*-I2%N z(B|-ugM2}QQ?vgSvKx|r4_NPJ#bx2a#GJf_cAB3n<)(z5CI~Z&uO@$?2^aO($Mx?S zI>SY*Dc-JTGBX#>KD?L7S8!2`+gsZMa)a^w$9!=SaKX!LD7&*CS}GMuUxsebyEo+& z=+IePLX?>$@)JCvG&A)F)&T3Pt0|$irsr&dyyaevmI74k8rL zOBILRq^ z%qXx%XKGJJ$E$p1tdgINfTR zQrHM%xlC)c;wG2)L~g)|?w6M!3m)@ZYa*;S;j4AAm7&{23$ST*Y{leTO^;l+ zQQtn&6M4c#W!F`!GwkY>b6cWy&(BwqL~59<%M`tS0+GOV6dRFA$Q3EK9xQCE;xD!1 zWcMd(M!8h)sr)n+_TGU1OI!O>fYiqNYLFpYL6vq+Lv6{lMBOPk3f9`)N&Hm7qri4>h0|MPSoIN-Ms*qs z51|0X49*7Xp2UuP(O^2aMf5W${JfgCc>d1k-hyMCR&T&Sv$149rkhix{fa=q z;+x-O8b|4PJUe0Y=t$rP(pYXTTXjngCo!}j!)_xMEacVsESJUExC5ER+2SBXnQ&mz*i#HaGjw;j|iFGvCwv4O-msv(Q|*j)DoM|o&bN?xsv zgCrSL#la~W77O~;}A+?_jR0lDc@G;~-vtwjr^Tc(4j$;-!>9fQS!_G6c7YqMf zWGsD<&-TkpWuu|tJ6LeBc?0&~FWNgNednOfcEQFIS=wf`Sw{BDLyzq_d+|&K6jZv3 zUfR{Qvc8cvZQ2Mr%;mv+>6ML0g7vi*#yhuG3ix);>%y)7X7m7oX9X=f6_M z>hgHd43H*O6r1mB73Gg}sxr+GjALSWXFTGVq-1~d=O|m$+?+iCJwhw#X-d< zmw?d6aH!|7mw&wtmUBU@>FaqmXGDCIK-O;bce1H?DN$qd^JxhlJv;Yy(&0|vJe!&2 zWn&*p=4mZ6G7Xnay4m_-3^W1@Gpl|r;X$u)Ho^%rcqk2-LA$(P`sw{;xG!9u>8IAL zuIlj$3Ku23DFNCS2@EFE;(!BCuT(awsv4($=bWjcuWqit^ZgA$gm#e9hmsXI2xJYi zkw6x$;LtNwp2{+jgSDe+!-_pg>8*a{^eGPgV&y6lr%OwQ&r!c;isUKTGkaov=jK^F zc!-@0`rA^C1^=a^S_2g*zpELmzl_68V2U7@D3P&Y!a$ISArIDP`ob#^b!)N|Cxr1; z#^&h!MQKS%b}52y%XY_K;{G&iEq^t3ryGLu`~Zy=glj_s7WLnO$t~3GxaV0u=takG zCN44Fw9n05k3C@<0|H+x3QJsF29?d8cY#)dpAg;$PVWcd6$qffln}N@%Y0rqxsd+V zcDOuBz2C|VsaR=0^cKNbTXwo_12Sty-{`F0iXMDd8#`I`X|0h0|Koyy%Kr8eXNN%K zADosVxAb4ydn{7_^Q!~lr2u{0WSRR$PFqbtH?TA8{LbXJ31mT zWut$80ihoA65BPpD-I!_zKbp%OUPT6sSnpMEB!xuZS|jB-nhQG6|_Zp2E}}QvH5VI zkBdkS1$Ghq6XRPun3rBsjxwF&K)cQoj=lX4z$b(9?&xnGW*HkLg78X&+!Q1glO<38@1me@OdEqJg_b*wv<2p(DjtZ!pVJyJ-edd z*ZYg1Zc^drO;l5yL|tK$>V4%ojjCOrYsE?g*LfN}W!Se1E zBjec9cPEWD$|2Pd7c@Mtq(sdus#Du;A5M@!un)_D!*%~N?lxJk)uJ~^X&&AWj^9(d2!sc&;sz>W zdJQ>d2oj83`S0w$6_43VxfV&*rvC@1vV`q@pvVcI*dSa~B%2)OYAN5wq!gu%z%t%1 z1#00_ZjmP;7pY~e5H!>jYx>7Qvhso`&dfXlj(U_bB95?b%!r9`U{Y`@xSN$<`R_Wg z12O47T-blYxD{R|{_5Em1QMuQ!s4Fs%gy)ZdsF)4X#SIVk-m=sI*4DuIfgLWU-IuR z>~)te1mvUPK|+W1dqg9kM4nSeE!$%@oS)sE5W*dkcI!r;dDB4e0OYy*8To`X-S*-? zzJLM;ZbE}n%o@Bw^$fyCy_Q&N;1fungxsnG`^~{j6g*=L0!_nPW||9lk}xjXjqw59r>PcTNtFov~J4#B|_F*c4Y}r@B>e zM1=i%@k6Y{!lLjL{TZTfZf*lnb@<4^9Jm6qL<|BPZeVotXjA^VqP*OBw?7;rxdgm< zSeslV#lI)~fk`-zPU`5#@K$d{EYfLCd{B^7K~!xGn>;#Q*{*hE` zH->NFpnP7uVG&<2GJQ9JLTLXkWmEhKHQY?$DQ}UPPS1<(QMyVv2;)=E$daiI($MU7 zXN{qSvc_lsLk67(5@ckBvBn(nN!$^Z!)8pqD(f3+n-h0NxBzpFbaK1qzP zCraeqJsG7MXRtR?@fM>3e5r4j~##ZeOR)=xa*DGw~vcRpxccpo}7!3)$AG z6m9cHkW7628c|wGQw<^sxC(M?&nu2G-|Y+b)ejr@K{#Q9zgmL+KHJzB$RnCNmLmg? z`rk@&`9DL;Ut3Asgj6PmBzIdeL1JgK{Fn9UL;rs2uh-W=JRhI`d<}l%udT2j&VPRQ zFVp+~-@p2)N%5bLaiW>E%+|&U7E~7up7=bNR z$XVWJ=!Jc68DCrRfBYYx>HotgDSzcEA}RuO_0@CV{{3}ydbwC<{^I-)IMXkBJa*)7 z;liY9SO2Vf-5s~4y7IxEKz4R^MUzdRlWtOO_D&g_y*%j^i4li~%h9V?9`81Rew}m{ zQJK!EwU(CF<0^W{^(|Uj3G13I7e1%evMpCU5|K$LHobb4S+Fq;G*ya(@SZB(4=1#9 z!{aF6H$|u;!W$q<^ssvG@^JjX`+2g(BQy^oP>?{N{jZK|^Cfo=x+; zjF2A5xf+$_Ku^n50@fQ~kR3WQYO0=#R+hRK`a!?acn2e~_Th=KcOco%E~5Mx&Cc3S zAqCy?hMA8i2?^3c2MswGYv{l2;glbJ+v4L*fXYO41WAVkO`HO{y*)$jU@8^?L41Z! zvrl~cmSaOKWrTdBIa>o^_kg4LTk}})87bN5+22fIyKVaiI~9CPP4@N&6k0WA_ei~t zfn?d>k|z}kqlQSOS<}&tmedH>t6Mm(!`qI;#EY~E4xXN#Ml9+sHbHP-?gGWXWDnax z7|)vG=`|0CG4kd9MiTPJq<94MMnyCGe6Xhp?QBw3sQ7#t!C;IN)Nu_S{i5e87LYj@ z4wQvk3g=V-R9tWO7pcP-N?m!`v+^0Vq7azAbQ=g>>lG_(J^!hBg z06``t)hA@x9u&N%dR2Xvh5;!*)RYMJr?Sts`*~1P^73x}#+2ow$9RT^kLPo@{g{ug zt+~F+)#7?QVz(Oe50cxPH`cMYwDjz1y5=+8%2x<30hK#|#zKl% zODQuUVKOF%_U`VEH+x?!l86wOQV6^BB+m_UFmcN8`>()r*WjQtPOS*)a%TihxSxeR=#-t zeALY2VsBbmLF;4l9}Co33rB~4x3St)T{q>5-XcWz%4+_`UyTlNc5`Wewb61L*~W&r z{)02P&)rD}IKAb}chE1J!_pj>N_MUcyl;3)jpcMxL0$G=C3L;j5WF>Hf~#6fK6B`o zS0H%7KZk9xL$Qc6u6nw6Mczhlzq8~p?TIlSW-fPcKG+$jMMB@BdJB{zxL`C-4!T~z7-9hsQ|)hEq4iJ zk&HkJd8E0i35QXq!lC=3wajgIBxest*L~W+0I<(B&hI3zuic2fj~>cyFBx=OMmjsO zI*zKgw}a3Qd>-%F^JLOoFQzv^5gWJI-(ONuxH9jeYBPH=+Dn;UQCrE0Wo%=5Ie7kA zaORiw-PPJR`7F3L6;z)JeXsZL+lR@5N<_687)qrOPb&un7Hal4sTFfY#JUwkO57iE zid0LRDmVJ~XKNX>s<^1Q{@f|o3``F$`m|?uBBAZwnsx9LRLujs@;haJeAjmjJi)?V&CytCWm0#&;zM%w~^&-#S6-qG6~+EWrZ(#ViCM%ao8XjB`u1J;;~csc$Ht) za&_lleMlXrEz5sTsrc~oF2k2Zco@fZ8K_lb$m!~Sc7FZd`w3Ts*Y$WVW9!V>_+-k% z&BrFKU-S1O7uQ|{(ObQZX!vMkZ@!r^LONg&(tvKJBgK) zvQ{c;Y89HeYd20ujS`_HiS>k@wQ=P*NKRe;r`%sJ&)eK1-A34F%<` zfkp4fQ`Kpn&9${L7&rH184x<0Wz_98w`%s63Gh?>-8NCH+)?YL8$)jK`Ex;I#BVZD z?cRFam(0iz7ePd5C_qEa(x)xLjnla2Cny+z+%{d4gmN9XW&$$wJuXfmjd>l*80djX zNUZ7);SpsIOWT)eD;Fq{OQiC+&H3#DF4?o1!S>+pqT<3m6{m#wg_*m3PS6r*qH5MD zo5UA?QZQ`xZGkq)U^gTar3oIXMo(8A(z`{0`_WX)M!9Jq?3H%S_r^3`Qc7yC#!4{c zV`gq{S6Zv3Y>EJ3M_7jY1T87O*X2Q)K{-I-KR>qc>;Kd*Ewxtq?}g>EJ;gmWx*axE zhd?5Mmp0(!NcR`U3JWIb_)79OlC$^;^jpaBFVC4fKjezcC$gfVNEBV?)H`Ml$!V!^ zeV#~F)$ypzZ78fdQ;~LxqaIwsMF}iQNl_!uO~*q)1di)RXO@z-D{$Eo z&1301Vdf>K`2469N!YcAl7)prbjb;j^PQWU%a2rmNF+WiHg-1W<*ELj0%vy5kMxnP zs+~gb)(9kB67$`Z9~7}Ja_FxxyJb>1I)b}9Y{Pb!+KiYA9EELqpdTs)TqeL#E>^Wb z+4;g*uQ3oQZ`?vUvU6oZB9`KbZ`&vug85e zq(8m>xV>?DMS(#lE#u?L@BQ9Cy>?q(ekpi=)mnFj;DLmstJ`|hz!U1*L6bJ1CZCp; zrWBAe)u2SjAOb}>JGvh6>;2b@{Ug5K zCP^RQAx9eC$+AZhdq*Wby|#=DkB6ypGMwW6;Wve|KE4YnvvI8sw;UE|{X=eUZuGk4 z_N$z_;}a85ThOjRMIZA=4vVXZsOGYs{^kb5+ldFH+qqf?4%R=k%M2EY3X0n7f)1o9 z{dTrXHPyjIMYJ8ie?Rqn?-U;Q>`>sB$~Bi>Zlc~19uW^gM{x^|?=gW)I+u!q0>Mk7 zmzbEmWL_ILPQ+%_DmMZpbse-8-{_|Xe>_F=^nsX^e{i3EQmeHt`M_}Kg8Tk}7Pd}K z<;GcBTU)CMvT~ce>Ku_a&WImyzP^XkZ!O>;g6=aOFYkwQuiJ~g*;Yy(ikd@qf51A~ z-+$H8b~XM*G^@U`G4!LC?$9AvauP97pJt!J!^K6c;-*>OJ&exGteA2%Qjv2Z>^=?; zQ((Kfx+bQdTzjH)fD?0eW+wj`A|#bFPp!$$$3+op>`sT_`icA%gi&3S+~o_kb$3s) z?S~ocroM~efLl~!1+3CHrmabe1xi{BlV{)C*_;ni*FXJI?htlcyt#4rYh$IOYiekC z1@*#U;E@3ZpQ55$>7#}hH4Iog)G1bXPvP1|VJSN&O;)Jr5uF@0z{kM$^2jW<3e_GB zg(6FR>U*4%`)XI2_?NS44rlZ6G#z=cl1NO=Li0s{bhlCAo0hi9JTZDzTz{IPxFOQG zWRoj>T$gA{cj5^EGR={ZPwFBfSWWxwoSX;=xlOwI)YVO8p0t4sS+&IdJqgAX zFIN~Imif7$K@7>X6i4hdℜ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"` to your blacklist to avoid issues, unless you are willing to maintain enough extra `BNB` on the account or unless you're willing to disable using `BNB` for fees. Binance accounts may use `BNB` for fees, and if a trade happens to be on `BNB`, further trades may consume this position and make the initial BNB trade unsellable as the expected amount is not there anymore. +### Binance sites + +Binance has been split into 2, and users must use the correct ccxt exchange ID for their exchange, otherwise API keys are not recognized. + +* [binance.com](https://www.binance.com/) - International users. Use exchange id: `binance`. +* [binance.us](https://www.binance.us/) - US based users. Use exchange id: `binanceus`. + ### Binance Futures Binance has specific (unfortunately complex) [Futures Trading Quantitative Rules](https://www.binance.com/en/support/faq/4f462ebe6ff445d4a170be7d9e897272) which need to be followed, and which prohibit a too low stake-amount (among others) for too many orders. @@ -87,12 +94,14 @@ When trading on Binance Futures market, orderbook must be used because there is }, ``` -### Binance sites +#### Binance futures settings -Binance has been split into 2, and users must use the correct ccxt exchange ID for their exchange, otherwise API keys are not recognized. +Users will also have to have the futures-setting "Position Mode" set to "One-way Mode", and "Asset Mode" set to "Single-Asset Mode". +These settings will be checked on startup, and freqtrade will show an error if this setting is wrong. -* [binance.com](https://www.binance.com/) - International users. Use exchange id: `binance`. -* [binance.us](https://www.binance.us/) - US based users. Use exchange id: `binanceus`. +![Binance futures settings](assets/binance_futures_settings.png) + +Freqtrade will not attempt to change these settings. ## Kraken diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index f9fb4a8b1..a0d4b2d82 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -68,6 +68,37 @@ class Binance(Exchange): tickers = deep_merge_dicts(bidsasks, tickers, allow_null_overrides=False) return tickers + @retrier + def additional_exchange_init(self) -> None: + """ + Additional exchange initialization logic. + .api will be available at this point. + Must be overridden in child methods if required. + """ + try: + if self.trading_mode == TradingMode.FUTURES and not self._config['dry_run']: + position_side = self._api.fapiPrivateGetPositionsideDual() + self._log_exchange_response('position_side_setting', position_side) + assets_margin = self._api.fapiPrivateGetMultiAssetsMargin() + self._log_exchange_response('multi_asset_margin', assets_margin) + msg = "" + if position_side.get('dualSidePosition') is True: + msg += ( + "\nHedge Mode is not supported by freqtrade. " + "Please change 'Position Mode' on your binance futures account.") + if assets_margin.get('multiAssetsMargin') is True: + msg += ("\nMulti-Asset Mode is not supported by freqtrade. " + "Please change 'Asset Mode' on your binance futures account.") + if msg: + raise OperationalException(msg) + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e + except (ccxt.NetworkError, ccxt.ExchangeError) as e: + raise TemporaryError( + f'Could not set leverage due to {e.__class__.__name__}. Message: {e}') from e + except ccxt.BaseError as e: + raise OperationalException(e) from e + @retrier def _set_leverage( self, diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index e9f4dfa8a..ef5cb1240 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -501,6 +501,24 @@ def test_fill_leverage_tiers_binance_dryrun(default_conf, mocker, leverage_tiers assert len(v) == len(value) +def test_additional_exchange_init_binance(default_conf, mocker): + api_mock = MagicMock() + api_mock.fapiPrivateGetPositionsideDual = MagicMock(return_value={"dualSidePosition": True}) + api_mock.fapiPrivateGetMultiAssetsMargin = MagicMock(return_value={"multiAssetsMargin": True}) + default_conf['dry_run'] = False + default_conf['trading_mode'] = TradingMode.FUTURES + default_conf['margin_mode'] = MarginMode.ISOLATED + with pytest.raises(OperationalException, + match=r"Hedge Mode is not supported.*\nMulti-Asset Mode is not supported.*"): + get_patched_exchange(mocker, default_conf, id="binance", api_mock=api_mock) + api_mock.fapiPrivateGetPositionsideDual = MagicMock(return_value={"dualSidePosition": False}) + api_mock.fapiPrivateGetMultiAssetsMargin = MagicMock(return_value={"multiAssetsMargin": False}) + exchange = get_patched_exchange(mocker, default_conf, id="binance", api_mock=api_mock) + assert exchange + ccxt_exceptionhandlers(mocker, default_conf, api_mock, 'binance', + "additional_exchange_init", "fapiPrivateGetPositionsideDual") + + def test__set_leverage_binance(mocker, default_conf): api_mock = MagicMock() diff --git a/tests/exchange/test_ccxt_compat.py b/tests/exchange/test_ccxt_compat.py index 82be6196a..6798cd2f7 100644 --- a/tests/exchange/test_ccxt_compat.py +++ b/tests/exchange/test_ccxt_compat.py @@ -137,6 +137,7 @@ def exchange_futures(request, exchange_conf, class_mocker): 'freqtrade.exchange.binance.Binance.fill_leverage_tiers') class_mocker.patch('freqtrade.exchange.exchange.Exchange.fetch_trading_fees') class_mocker.patch('freqtrade.exchange.okx.Okx.additional_exchange_init') + class_mocker.patch('freqtrade.exchange.binance.Binance.additional_exchange_init') class_mocker.patch('freqtrade.exchange.exchange.Exchange.load_cached_leverage_tiers', return_value=None) class_mocker.patch('freqtrade.exchange.exchange.Exchange.cache_leverage_tiers') From 201bbbcee67d33a70dc81e1a04743345fedacd35 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 1 Oct 2022 09:32:16 +0200 Subject: [PATCH 060/126] Okx formatting --- freqtrade/exchange/okx.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/okx.py b/freqtrade/exchange/okx.py index fe1c94017..6792c2cba 100644 --- a/freqtrade/exchange/okx.py +++ b/freqtrade/exchange/okx.py @@ -78,7 +78,8 @@ class Okx(Exchange): raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( - f'Error in additional_exchange_init due to {e.__class__.__name__}. Message: {e}') from e + f'Error in additional_exchange_init due to {e.__class__.__name__}. Message: {e}' + ) from e except ccxt.BaseError as e: raise OperationalException(e) from e From 8f8b5cc28ef82ddeed11c216058cd0c1c47ee710 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 1 Oct 2022 09:35:21 +0200 Subject: [PATCH 061/126] Disable log spam from analyze_df in webhook/discord --- freqtrade/rpc/discord.py | 2 +- freqtrade/rpc/webhook.py | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/discord.py b/freqtrade/rpc/discord.py index 9efe6f427..c48508300 100644 --- a/freqtrade/rpc/discord.py +++ b/freqtrade/rpc/discord.py @@ -30,9 +30,9 @@ class Discord(Webhook): pass def send_msg(self, msg) -> None: - logger.info(f"Sending discord message: {msg}") if msg['type'].value in self.config['discord']: + logger.info(f"Sending discord message: {msg}") msg['strategy'] = self.strategy msg['timeframe'] = self.timeframe diff --git a/freqtrade/rpc/webhook.py b/freqtrade/rpc/webhook.py index 6109e80bc..bb3b3922f 100644 --- a/freqtrade/rpc/webhook.py +++ b/freqtrade/rpc/webhook.py @@ -61,6 +61,14 @@ class Webhook(RPCHandler): RPCMessageType.STARTUP, RPCMessageType.WARNING): valuedict = whconfig.get('webhookstatus') + elif msg['type'] in ( + RPCMessageType.PROTECTION_TRIGGER, + RPCMessageType.PROTECTION_TRIGGER_GLOBAL, + RPCMessageType.WHITELIST, + RPCMessageType.ANALYZED_DF, + RPCMessageType.STRATEGY_MSG): + # Don't fail for non-implemented types + return else: raise NotImplementedError('Unknown message type: {}'.format(msg['type'])) if not valuedict: From 6702a1b21905be3a6a8b00222c20e2c5e09bc449 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 1 Oct 2022 09:45:58 +0200 Subject: [PATCH 062/126] Update test to verify webhook won't log-spam on new messagetypes --- tests/rpc/test_rpc_webhook.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/rpc/test_rpc_webhook.py b/tests/rpc/test_rpc_webhook.py index 4d65b4966..3bbb85d54 100644 --- a/tests/rpc/test_rpc_webhook.py +++ b/tests/rpc/test_rpc_webhook.py @@ -365,6 +365,14 @@ def test_exception_send_msg(default_conf, mocker, caplog): with pytest.raises(NotImplementedError): webhook.send_msg(msg) + # Test no failure for not implemented but known messagetypes + for e in RPCMessageType: + msg = { + 'type': e, + 'status': 'whatever' + } + webhook.send_msg(msg) + def test__send_msg(default_conf, mocker, caplog): default_conf["webhook"] = get_webhook_dict() From f2b875483f671dca812f2298ea1177ca1433b823 Mon Sep 17 00:00:00 2001 From: robcaulk Date: Sat, 1 Oct 2022 13:14:59 +0200 Subject: [PATCH 063/126] ensure raw features match when PCA is employed --- freqtrade/freqai/data_kitchen.py | 4 ---- freqtrade/freqai/freqai_interface.py | 13 +++++-------- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/freqtrade/freqai/data_kitchen.py b/freqtrade/freqai/data_kitchen.py index c05900bad..766eb981f 100644 --- a/freqtrade/freqai/data_kitchen.py +++ b/freqtrade/freqai/data_kitchen.py @@ -1,6 +1,5 @@ import copy import logging -import re import shutil from datetime import datetime, timezone from math import cos, sin @@ -882,9 +881,6 @@ class FreqaiDataKitchen: """ column_names = dataframe.columns features = [c for c in column_names if "%" in c] - pca_features = [c for c in column_names if re.search(r"^PC\d+$", c)] - if not features and pca_features: - features = pca_features if not features: raise OperationalException("Could not find any features!") diff --git a/freqtrade/freqai/freqai_interface.py b/freqtrade/freqai/freqai_interface.py index bf625b2a7..5cc6d3f69 100644 --- a/freqtrade/freqai/freqai_interface.py +++ b/freqtrade/freqai/freqai_interface.py @@ -275,7 +275,8 @@ class IFreqaiModel(ABC): if dk.check_if_backtest_prediction_exists(): self.dd.load_metadata(dk) - self.check_if_feature_list_matches_strategy(dataframe_train, dk) + dk.find_features(dataframe_train) + self.check_if_feature_list_matches_strategy(dk) append_df = dk.get_backtesting_prediction() dk.append_predictions(append_df) else: @@ -296,7 +297,6 @@ class IFreqaiModel(ABC): else: self.model = self.dd.load_data(pair, dk) - # self.check_if_feature_list_matches_strategy(dataframe_train, dk) pred_df, do_preds = self.predict(dataframe_backtest, dk) append_df = dk.get_predictions_to_append(pred_df, do_preds) dk.append_predictions(append_df) @@ -420,7 +420,7 @@ class IFreqaiModel(ABC): return def check_if_feature_list_matches_strategy( - self, dataframe: DataFrame, dk: FreqaiDataKitchen + self, dk: FreqaiDataKitchen ) -> None: """ Ensure user is passing the proper feature set if they are reusing an `identifier` pointing @@ -429,15 +429,12 @@ class IFreqaiModel(ABC): :param dk: FreqaiDataKitchen = non-persistent data container/analyzer for current coin/bot loop """ - dk.find_features(dataframe) + if "training_features_list_raw" in dk.data: feature_list = dk.data["training_features_list_raw"] else: feature_list = dk.data['training_features_list'] - if self.ft_params.get('principal_component_analysis', False): - feature_list = dk.data['training_features_list'] - if dk.training_features_list != feature_list: raise OperationalException( "Trying to access pretrained model with `identifier` " @@ -510,7 +507,7 @@ class IFreqaiModel(ABC): dk.use_DBSCAN_to_remove_outliers(predict=True) # ensure user is feeding the correct indicators to the model - self.check_if_feature_list_matches_strategy(dk.data_dictionary['prediction_features'], dk) + self.check_if_feature_list_matches_strategy(dk) def model_exists(self, dk: FreqaiDataKitchen) -> bool: """ From cd514cf15d35aa53774070c3bdd40ffd0f67ec00 Mon Sep 17 00:00:00 2001 From: robcaulk Date: Sat, 1 Oct 2022 14:18:46 +0200 Subject: [PATCH 064/126] fix inlier metric in backtesting --- freqtrade/freqai/base_models/BaseClassifierModel.py | 2 +- freqtrade/freqai/base_models/BaseRegressionModel.py | 2 +- freqtrade/freqai/data_drawer.py | 2 +- freqtrade/freqai/data_kitchen.py | 2 ++ freqtrade/freqai/freqai_interface.py | 8 ++++---- 5 files changed, 9 insertions(+), 7 deletions(-) diff --git a/freqtrade/freqai/base_models/BaseClassifierModel.py b/freqtrade/freqai/base_models/BaseClassifierModel.py index 70f212d2a..09f1bf98c 100644 --- a/freqtrade/freqai/base_models/BaseClassifierModel.py +++ b/freqtrade/freqai/base_models/BaseClassifierModel.py @@ -92,7 +92,7 @@ class BaseClassifierModel(IFreqaiModel): filtered_df = dk.normalize_data_from_metadata(filtered_df) dk.data_dictionary["prediction_features"] = filtered_df - self.data_cleaning_predict(dk, filtered_df) + self.data_cleaning_predict(dk) predictions = self.model.predict(dk.data_dictionary["prediction_features"]) pred_df = DataFrame(predictions, columns=dk.label_list) diff --git a/freqtrade/freqai/base_models/BaseRegressionModel.py b/freqtrade/freqai/base_models/BaseRegressionModel.py index 2450bf305..5d89dd356 100644 --- a/freqtrade/freqai/base_models/BaseRegressionModel.py +++ b/freqtrade/freqai/base_models/BaseRegressionModel.py @@ -92,7 +92,7 @@ class BaseRegressionModel(IFreqaiModel): dk.data_dictionary["prediction_features"] = filtered_df # optional additional data cleaning/analysis - self.data_cleaning_predict(dk, filtered_df) + self.data_cleaning_predict(dk) predictions = self.model.predict(dk.data_dictionary["prediction_features"]) pred_df = DataFrame(predictions, columns=dk.label_list) diff --git a/freqtrade/freqai/data_drawer.py b/freqtrade/freqai/data_drawer.py index 1839724f8..471f6875c 100644 --- a/freqtrade/freqai/data_drawer.py +++ b/freqtrade/freqai/data_drawer.py @@ -423,7 +423,7 @@ class FreqaiDataDrawer: dk.data["data_path"] = str(dk.data_path) dk.data["model_filename"] = str(dk.model_filename) - dk.data["training_features_list"] = list(dk.data_dictionary["train_features"].columns) + dk.data["training_features_list"] = dk.training_features_list dk.data["label_list"] = dk.label_list # store the metadata with open(save_path / f"{dk.model_filename}_metadata.json", "w") as fp: diff --git a/freqtrade/freqai/data_kitchen.py b/freqtrade/freqai/data_kitchen.py index 766eb981f..7efefd127 100644 --- a/freqtrade/freqai/data_kitchen.py +++ b/freqtrade/freqai/data_kitchen.py @@ -844,10 +844,12 @@ class FreqaiDataKitchen: 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) + # self.find_features(self.data_dictionary[f'{set_}_features']) else: self.data_dictionary['prediction_features'] = pd.concat( [compute_df, inlier_metric], axis=1) self.data_dictionary['prediction_features'].fillna(0, inplace=True) + # self.find_features(self.data_dictionary['prediction_features']) logger.info('Inlier metric computed and added to features.') diff --git a/freqtrade/freqai/freqai_interface.py b/freqtrade/freqai/freqai_interface.py index 5cc6d3f69..78539bae5 100644 --- a/freqtrade/freqai/freqai_interface.py +++ b/freqtrade/freqai/freqai_interface.py @@ -482,13 +482,16 @@ class IFreqaiModel(ABC): 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: + def data_cleaning_predict(self, dk: FreqaiDataKitchen) -> None: """ Base data cleaning method for predict. Functions here are complementary to the functions of data_cleaning_train. """ ft_params = self.freqai_info["feature_parameters"] + # ensure user is feeding the correct indicators to the model + self.check_if_feature_list_matches_strategy(dk) + if ft_params.get('inlier_metric_window', 0): dk.compute_inlier_metric(set_='predict') @@ -506,9 +509,6 @@ class IFreqaiModel(ABC): if ft_params.get("use_DBSCAN_to_remove_outliers", False): dk.use_DBSCAN_to_remove_outliers(predict=True) - # ensure user is feeding the correct indicators to the model - self.check_if_feature_list_matches_strategy(dk) - def model_exists(self, dk: FreqaiDataKitchen) -> bool: """ Given a pair and path, check if a model already exists From f4c6b99d63b6da87da9318cbf599e074fa6a50e0 Mon Sep 17 00:00:00 2001 From: robcaulk Date: Sat, 1 Oct 2022 14:23:15 +0200 Subject: [PATCH 065/126] remove commented lines --- freqtrade/freqai/data_kitchen.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/freqtrade/freqai/data_kitchen.py b/freqtrade/freqai/data_kitchen.py index 7efefd127..766eb981f 100644 --- a/freqtrade/freqai/data_kitchen.py +++ b/freqtrade/freqai/data_kitchen.py @@ -844,12 +844,10 @@ class FreqaiDataKitchen: 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) - # self.find_features(self.data_dictionary[f'{set_}_features']) else: self.data_dictionary['prediction_features'] = pd.concat( [compute_df, inlier_metric], axis=1) self.data_dictionary['prediction_features'].fillna(0, inplace=True) - # self.find_features(self.data_dictionary['prediction_features']) logger.info('Inlier metric computed and added to features.') From 2c94ed2e59c5ef7eeb6a33c2caea045ad0a3e491 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 1 Oct 2022 21:20:14 +0200 Subject: [PATCH 066/126] Decrease message throughput fixes memory leak by queue raising indefinitely --- freqtrade/rpc/api_server/webserver.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/api_server/webserver.py b/freqtrade/rpc/api_server/webserver.py index df4324740..53af91477 100644 --- a/freqtrade/rpc/api_server/webserver.py +++ b/freqtrade/rpc/api_server/webserver.py @@ -198,8 +198,10 @@ class ApiServer(RPCHandler): logger.debug(f"Found message of type: {message.get('type')}") # Broadcast it await self._ws_channel_manager.broadcast(message) - # Sleep, make this configurable? - await asyncio.sleep(0.1) + # Limit messages per sec. + # Could cause problems with queue size if too low, and + # problems with network traffik if too high. + await asyncio.sleep(0.001) except asyncio.CancelledError: pass From 564318415eb5d9ed41a3b8c1e85801d65bea7856 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 2 Oct 2022 08:12:03 +0200 Subject: [PATCH 067/126] Improve test resiliance --- tests/rpc/test_rpc_emc.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/rpc/test_rpc_emc.py b/tests/rpc/test_rpc_emc.py index 28adc66b9..b73a64f06 100644 --- a/tests/rpc/test_rpc_emc.py +++ b/tests/rpc/test_rpc_emc.py @@ -207,12 +207,15 @@ async def test_emc_create_connection_invalid_port(default_conf, caplog, mocker): }) dp = DataProvider(default_conf, None, None, None) + # Handle start explicitly to avoid messing with threading in tests + mocker.patch("freqtrade.rpc.external_message_consumer.ExternalMessageConsumer.start",) emc = ExternalMessageConsumer(default_conf, dp) try: - await asyncio.sleep(0.01) + await emc._create_connection(emc.producers[0], asyncio.Lock()) assert log_has_re(r".+ is an invalid WebSocket URL .+", caplog) finally: + emc._running = False emc.shutdown() From 308fa430078bea9719fdb40a24cec6cca4c5c0f5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 2 Oct 2022 08:30:19 +0200 Subject: [PATCH 068/126] Don't use magicmock as trade object --- tests/test_freqtradebot.py | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 0f1a05ab4..ca9e63890 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -28,6 +28,7 @@ from tests.conftest import (create_mock_trades, create_mock_trades_usdt, get_pat from tests.conftest_trades import (MOCK_TRADE_COUNT, entry_side, exit_side, mock_order_1, mock_order_2, mock_order_2_sell, mock_order_3, mock_order_3_sell, mock_order_4, mock_order_5_stoploss, mock_order_6_sell) +from tests.conftest_trades_usdt import mock_trade_usdt_4 def patch_RPCManager(mocker) -> MagicMock: @@ -2980,7 +2981,7 @@ def test_manage_open_orders_exception(default_conf_usdt, ticker_usdt, open_trade @pytest.mark.parametrize("is_short", [False, True]) -def test_handle_cancel_enter(mocker, caplog, default_conf_usdt, limit_order, is_short) -> None: +def test_handle_cancel_enter(mocker, caplog, default_conf_usdt, limit_order, is_short, fee) -> None: patch_RPCManager(mocker) patch_exchange(mocker) l_order = limit_order[entry_side(is_short)] @@ -2994,16 +2995,12 @@ def test_handle_cancel_enter(mocker, caplog, default_conf_usdt, limit_order, is_ freqtrade = FreqtradeBot(default_conf_usdt) freqtrade._notify_enter_cancel = MagicMock() - # TODO: Convert to real trade - trade = MagicMock() - trade.pair = 'LTC/USDT' - trade.open_rate = 200 - trade.is_short = False - trade.entry_side = "buy" - trade.amount = 100 + trade = mock_trade_usdt_4(fee, is_short) + Trade.query.session.add(trade) + Trade.commit() + l_order['filled'] = 0.0 l_order['status'] = 'open' - trade.nr_of_successful_entries = 0 reason = CANCEL_REASON['TIMEOUT'] assert freqtrade.handle_cancel_enter(trade, l_order, reason) assert cancel_order_mock.call_count == 1 @@ -3035,7 +3032,7 @@ def test_handle_cancel_enter(mocker, caplog, default_conf_usdt, limit_order, is_ @pytest.mark.parametrize("is_short", [False, True]) @pytest.mark.parametrize("limit_buy_order_canceled_empty", ['binance', 'ftx', 'kraken', 'bittrex'], indirect=['limit_buy_order_canceled_empty']) -def test_handle_cancel_enter_exchanges(mocker, caplog, default_conf_usdt, is_short, +def test_handle_cancel_enter_exchanges(mocker, caplog, default_conf_usdt, is_short, fee, limit_buy_order_canceled_empty) -> None: patch_RPCManager(mocker) patch_exchange(mocker) @@ -3046,11 +3043,10 @@ def test_handle_cancel_enter_exchanges(mocker, caplog, default_conf_usdt, is_sho freqtrade = FreqtradeBot(default_conf_usdt) reason = CANCEL_REASON['TIMEOUT'] - # TODO: Convert to real trade - trade = MagicMock() - trade.nr_of_successful_entries = 0 - trade.pair = 'LTC/ETH' - trade.entry_side = "sell" if is_short else "buy" + + trade = mock_trade_usdt_4(fee, is_short) + Trade.query.session.add(trade) + Trade.commit() assert freqtrade.handle_cancel_enter(trade, limit_buy_order_canceled_empty, reason) assert cancel_order_mock.call_count == 0 assert log_has_re( From 9bb061073d541867892d7736cba57a7a46b1b96d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 2 Oct 2022 08:36:34 +0200 Subject: [PATCH 069/126] Improve tests --- freqtrade/exchange/exchange.py | 2 +- tests/test_freqtradebot.py | 24 +++++++++++++----------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index f01e464fa..61a6efb45 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1292,7 +1292,7 @@ class Exchange: order = self.fetch_order(order_id, pair) except InvalidOrderException: logger.warning(f"Could not fetch cancelled order {order_id}.") - order = {'fee': {}, 'status': 'canceled', 'amount': amount, 'info': {}} + order = {'id': order_id, 'fee': {}, 'status': 'canceled', 'amount': amount, 'info': {}} return order diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index ca9e63890..e19436a9f 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1061,6 +1061,7 @@ def test_add_stoploss_on_exchange(mocker, default_conf_usdt, limit_order, is_sho freqtrade = FreqtradeBot(default_conf_usdt) freqtrade.strategy.order_types['stoploss_on_exchange'] = True + # TODO: should not be magicmock trade = MagicMock() trade.is_short = is_short trade.open_order_id = None @@ -1102,6 +1103,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_ # First case: when stoploss is not yet set but the order is open # should get the stoploss order id immediately # and should return false as no trade actually happened + # TODO: should not be magicmock trade = MagicMock() trade.is_short = is_short trade.is_open = True @@ -1880,6 +1882,7 @@ def test_exit_positions(mocker, default_conf_usdt, limit_order, is_short, caplog return_value=limit_order[entry_side(is_short)]) mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[]) + # TODO: should not be magicmock trade = MagicMock() trade.is_short = is_short trade.open_order_id = '123' @@ -1903,6 +1906,7 @@ def test_exit_positions_exception(mocker, default_conf_usdt, limit_order, caplog order = limit_order[entry_side(is_short)] mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=order) + # TODO: should not be magicmock trade = MagicMock() trade.is_short = is_short trade.open_order_id = None @@ -2043,6 +2047,7 @@ def test_update_trade_state_exception(mocker, default_conf_usdt, is_short, limit freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=order) + # TODO: should not be magicmock trade = MagicMock() trade.open_order_id = '123' trade.amount = 123 @@ -2061,6 +2066,7 @@ def test_update_trade_state_orderexception(mocker, default_conf_usdt, caplog) -> mocker.patch('freqtrade.exchange.Exchange.fetch_order', MagicMock(side_effect=InvalidOrderException)) + # TODO: should not be magicmock trade = MagicMock() trade.open_order_id = '123' @@ -3064,7 +3070,7 @@ def test_handle_cancel_enter_exchanges(mocker, caplog, default_conf_usdt, is_sho 'String Return value', 123 ]) -def test_handle_cancel_enter_corder_empty(mocker, default_conf_usdt, limit_order, is_short, +def test_handle_cancel_enter_corder_empty(mocker, default_conf_usdt, limit_order, is_short, fee, cancelorder) -> None: patch_RPCManager(mocker) patch_exchange(mocker) @@ -3072,20 +3078,15 @@ def test_handle_cancel_enter_corder_empty(mocker, default_conf_usdt, limit_order cancel_order_mock = MagicMock(return_value=cancelorder) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - cancel_order=cancel_order_mock + cancel_order=cancel_order_mock, + fetch_order=MagicMock(side_effect=InvalidOrderException) ) freqtrade = FreqtradeBot(default_conf_usdt) freqtrade._notify_enter_cancel = MagicMock() - # TODO: Convert to real trade - trade = MagicMock() - trade.pair = 'LTC/USDT' - trade.entry_side = "buy" - trade.open_rate = 200 - trade.entry_side = "buy" - trade.open_order_id = "open_order_noop" - trade.nr_of_successful_entries = 0 - trade.amount = 100 + trade = mock_trade_usdt_4(fee, is_short) + Trade.query.session.add(trade) + Trade.commit() l_order['filled'] = 0.0 l_order['status'] = 'open' reason = CANCEL_REASON['TIMEOUT'] @@ -3200,6 +3201,7 @@ def test_handle_cancel_exit_cancel_exception(mocker, default_conf_usdt) -> None: freqtrade = FreqtradeBot(default_conf_usdt) + # TODO: should not be magicmock trade = MagicMock() reason = CANCEL_REASON['TIMEOUT'] order = {'remaining': 1, From e686faf1bc8afa77327a9c1ae3774c8eb87716b4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 2 Oct 2022 08:37:37 +0200 Subject: [PATCH 070/126] Remove faulty test cleanup --- tests/rpc/test_rpc_emc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/rpc/test_rpc_emc.py b/tests/rpc/test_rpc_emc.py index b73a64f06..84a2658a0 100644 --- a/tests/rpc/test_rpc_emc.py +++ b/tests/rpc/test_rpc_emc.py @@ -212,10 +212,10 @@ async def test_emc_create_connection_invalid_port(default_conf, caplog, mocker): emc = ExternalMessageConsumer(default_conf, dp) try: + emc._running = True await emc._create_connection(emc.producers[0], asyncio.Lock()) assert log_has_re(r".+ is an invalid WebSocket URL .+", caplog) finally: - emc._running = False emc.shutdown() From d0b8c8b1a0a1c39f062d4e756ac5c128302ae287 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 2 Oct 2022 08:45:41 +0200 Subject: [PATCH 071/126] improve invalid canceled order response handling --- freqtrade/exchange/exchange.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 61a6efb45..5648d8716 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1292,7 +1292,14 @@ class Exchange: order = self.fetch_order(order_id, pair) except InvalidOrderException: logger.warning(f"Could not fetch cancelled order {order_id}.") - order = {'id': order_id, 'fee': {}, 'status': 'canceled', 'amount': amount, 'info': {}} + order = { + 'id': order_id, + 'status': 'canceled', + 'amount': amount, + 'filled': 0.0, + 'fee': {}, + 'info': {} + } return order From 21440eaec2e0a05fb16fbc7856921359418866ce Mon Sep 17 00:00:00 2001 From: th0rntwig Date: Sun, 2 Oct 2022 12:47:58 +0200 Subject: [PATCH 072/126] Fix typos and correct/improve descriptions --- docs/freqai-configuration.md | 6 ++-- docs/freqai-developers.md | 8 ++--- docs/freqai-feature-engineering.md | 6 ++-- docs/freqai-parameter-table.md | 20 ++++++------ docs/freqai-running.md | 50 +++++++++++++++--------------- docs/freqai.md | 13 ++++---- 6 files changed, 51 insertions(+), 52 deletions(-) diff --git a/docs/freqai-configuration.md b/docs/freqai-configuration.md index 50e75b658..683fc9b34 100644 --- a/docs/freqai-configuration.md +++ b/docs/freqai-configuration.md @@ -166,11 +166,11 @@ Below are the values you can expect to include/use inside a typical strategy dat | DataFrame Key | Description | |------------|-------------| -| `df['&*']` | Any dataframe column prepended with `&` in `populate_any_indicators()` is treated as a training target (label) inside `FreqAI` (typically following the naming convention `&-s*`). The names of these dataframe columns are fed back as the predictions. For example, to predict the price change in the next 40 candles (similar to `templates/FreqaiExampleStrategy.py`), you would set `df['&-s_close']`. `FreqAI` makes the predictions and gives them back under the same key (`df['&-s_close']`) to be used in `populate_entry/exit_trend()`.
**Datatype:** Depends on the output of the model. +| `df['&*']` | Any dataframe column prepended with `&` in `populate_any_indicators()` is treated as a training target (label) inside `FreqAI` (typically following the naming convention `&-s*`). For example, to predict the close price 40 candles into the future, you would set `df['&-s_close'] = df['close'].shift(-self.freqai_info["feature_parameters"]["label_period_candles"])` with `"label_period_candles": 40` in the config. `FreqAI` makes the predictions and gives them back under the same key (`df['&-s_close']`) to be used in `populate_entry/exit_trend()`.
**Datatype:** Depends on the output of the model. | `df['&*_std/mean']` | Standard deviation and mean values of the defined labels during training (or live tracking with `fit_live_predictions_candles`). Commonly used to understand the rarity of a prediction (use the z-score as shown in `templates/FreqaiExampleStrategy.py` and explained [here](#creating-a-dynamic-target-threshold) to evaluate how often a particular prediction was observed during training or historically with `fit_live_predictions_candles`).
**Datatype:** Float. -| `df['do_predict']` | Indication of an outlier data point. The return value is integer between -1 and 2, which lets you know if the prediction is trustworthy or not. `do_predict==1` means that the prediction is trustworthy. If the Dissimilarity Index (DI, see details [here](freqai-feature-engineering.md#identifying-outliers-with-the-dissimilarity-index-di)) of the input data point is above the threshold defined in the config, `FreqAI` will subtract 1 from `do_predict`, resulting in `do_predict==0`. If `use_SVM_to_remove_outliers()` is active, the Support Vector Machine (SVM, see details [here](freqai-feature-engineering.md#identifying-outliers-using-a-support-vector-machine-svm)) may also detect outliers in training and prediction data. In this case, the SVM will also subtract 1 from `do_predict`. If the input data point was considered an outlier by the SVM but not by the DI, or vice versa, the result will be `do_predict==0`. If both the DI and the SVM considers the input data point to be an outlier, the result will be `do_predict==-1`. A particular case is when `do_predict == 2`, which means that the model has expired due to exceeding `expired_hours`.
**Datatype:** Integer between -1 and 2. +| `df['do_predict']` | Indication of an outlier data point. The return value is integer between -2 and 2, which lets you know if the prediction is trustworthy or not. `do_predict==1` means that the prediction is trustworthy. If the Dissimilarity Index (DI, see details [here](freqai-feature-engineering.md#identifying-outliers-with-the-dissimilarity-index-di)) of the input data point is above the threshold defined in the config, `FreqAI` will subtract 1 from `do_predict`, resulting in `do_predict==0`. If `use_SVM_to_remove_outliers()` is active, the Support Vector Machine (SVM, see details [here](freqai-feature-engineering.md#identifying-outliers-using-a-support-vector-machine-svm)) may also detect outliers in training and prediction data. In this case, the SVM will also subtract 1 from `do_predict`. If the input data point was considered an outlier by the SVM but not by the DI, or vice versa, the result will be `do_predict==0`. If both the DI and the SVM considers the input data point to be an outlier, the result will be `do_predict==-1`. As with the SVM, if `use_DBSCAN_to_remove_outliers` is active, DBSCAN (see details [here](freqai-feature-engineering.md#identifying-outliers-with-dbscan)) may also detect outliers and subtract 1 from `do_predict`. Hence, if both the SVM and DBSCAN are active and identify a datapoint that was above the DI threshold as an outlier, the result will be `do_predict==-2`. A particular case is when `do_predict == 2`, which means that the model has expired due to exceeding `expired_hours`.
**Datatype:** Integer between -2 and 2. | `df['DI_values']` | Dissimilarity Index (DI) values are proxies for the level of confidence `FreqAI` has in the prediction. A lower DI means the prediction is close to the training data, i.e., higher prediction confidence. See details about the DI [here](freqai-feature-engineering.md#identifying-outliers-with-the-dissimilarity-index-di).
**Datatype:** Float. -| `df['%*']` | Any dataframe column prepended with `%` in `populate_any_indicators()` is treated as a training feature. For example, you can include the RSI in the training feature set (similar to in `templates/FreqaiExampleStrategy.py`) by setting `df['%-rsi']`. See more details on how this is done [here](freqai-feature-engineering.md).
**Note:** Since the number of features prepended with `%` can multiply very quickly (10s of thousands of features is easily engineered using the multiplictative functionality described in the `feature_parameters` table shown above), these features are removed from the dataframe upon return from `FreqAI`. To keep a particular type of feature for plotting purposes, you would prepend it with `%%`.
**Datatype:** Depends on the output of the model. +| `df['%*']` | Any dataframe column prepended with `%` in `populate_any_indicators()` is treated as a training feature. For example, you can include the RSI in the training feature set (similar to in `templates/FreqaiExampleStrategy.py`) by setting `df['%-rsi']`. See more details on how this is done [here](freqai-feature-engineering.md).
**Note:** Since the number of features prepended with `%` can multiply very quickly (10s of thousands of features are easily engineered using the multiplictative functionality of, e.g., `include_shifted_candles` and `include_timeframes` as described in the [parameter table](freqai-parameter-table.md)), these features are removed from the dataframe that is returned from `FreqAI` to the strategy. To keep a particular type of feature for plotting purposes, you would prepend it with `%%`.
**Datatype:** Depends on the output of the model. ## Setting the `startup_candle_count` diff --git a/docs/freqai-developers.md b/docs/freqai-developers.md index 4bff46f2f..9794e0efa 100644 --- a/docs/freqai-developers.md +++ b/docs/freqai-developers.md @@ -27,13 +27,13 @@ The file structure is automatically generated based on the model `identifier` se | Structure | Description | |-----------|-------------| | `config_*.json` | A copy of the model specific configuration file. | -| `historic_predictions.pkl` | A file containing all historic predictions generated during the lifetime of the `identifier` model during live deployment. `historic_predictions.pkl` is used to reload the model after a crash or a config change. A backup file is always held incase of corruption on the main file. **`FreqAI` automatically detects corruption and replaces the corrupted file with the backup**. | +| `historic_predictions.pkl` | A file containing all historic predictions generated during the lifetime of the `identifier` model during live deployment. `historic_predictions.pkl` is used to reload the model after a crash or a config change. A backup file is always held in case of corruption on the main file.`FreqAI`**automatically** detects corruption and replaces the corrupted file with the backup. | | `pair_dictionary.json` | A file containing the training queue as well as the on disk location of the most recently trained model. | | `sub-train-*_TIMESTAMP` | A folder containing all the files associated with a single model, such as:
-|| `*_metadata.json` - Metadata for the model, such as normalization max/mins, expected training feature list, etc.
+|| `*_metadata.json` - Metadata for the model, such as normalization max/min, expected training feature list, etc.
|| `*_model.*` - The model file saved to disk for reloading from a crash. Can be `joblib` (typical boosting libs), `zip` (stable_baselines), `hd5` (keras type), etc.
-|| `*_pca_object.pkl` - The [Principal component analysis (PCA)](freqai-feature-engineering.md#data-dimensionality-reduction-with-principal-component-analysis) transform (if `principal_component_analysis: true` is set in the config) which will be used to transform unseen prediction features.
-|| `*_svm_model.pkl` - The [Support Vector Machine (SVM)](freqai-feature-engineering.md#identifying-outliers-using-a-support-vector-machine-svm) model which is used to detect outliers in unseen prediction features.
+|| `*_pca_object.pkl` - The [Principal component analysis (PCA)](freqai-feature-engineering.md#data-dimensionality-reduction-with-principal-component-analysis) transform (if `principal_component_analysis: True` is set in the config) which will be used to transform unseen prediction features.
+|| `*_svm_model.pkl` - The [Support Vector Machine (SVM)](freqai-feature-engineering.md#identifying-outliers-using-a-support-vector-machine-svm) model (if `use_SVM_to_remove_outliers: True` is set in the config) which is used to detect outliers in unseen prediction features.
|| `*_trained_df.pkl` - The dataframe containing all the training features used to train the `identifier` model. This is used for computing the [Dissimilarity Index (DI)](freqai-feature-engineering.md#identifying-outliers-with-the-dissimilarity-index-di) and can also be used for post-processing.
|| `*_trained_dates.df.pkl` - The dates associated with the `trained_df.pkl`, which is useful for post-processing. | diff --git a/docs/freqai-feature-engineering.md b/docs/freqai-feature-engineering.md index 8f061b9fd..bd700bbf7 100644 --- a/docs/freqai-feature-engineering.md +++ b/docs/freqai-feature-engineering.md @@ -4,7 +4,7 @@ Low level feature engineering is performed in the user strategy within a function called `populate_any_indicators()`. That function sets the `base features` such as, `RSI`, `MFI`, `EMA`, `SMA`, time of day, volume, etc. The `base features` can be custom indicators or they can be imported from any technical-analysis library that you can find. One important syntax rule is that all `base features` string names are prepended with `%`, while labels/targets are prepended with `&`. -Meanwhile, high level feature engineering is handled within `"feature_parameters":{}` in the `FreqAI` config. Within this file, it is possible to decide large scale feature expansions on top of the `base_features` such as "including correlated pairs" or "including informative timeframes" or even "including recent candles." +Meanwhile, high level feature engineering is handled within `"feature_parameters":{}` in the `FreqAI` config. Within this file, it is possible to decide large scale feature expansions on top of the `base_features` such as "including correlated pairs" or "including informative timeframes" or even "including recent candles." It is advisable to start from the template `populate_any_indicators()` in the source provided example strategy (found in `templates/FreqaiExampleStrategy.py`) to ensure that the feature definitions are following the correct conventions. Here is an example of how to set the indicators and labels in the strategy: @@ -141,7 +141,7 @@ Another example, where the user wants to use live metrics from the trade databas } ``` -You need to set the standard dictionary in the config so that `FreqAI` can return proper dataframe shapes. These values will likely be overridden by the prediction model, but in the case where the model has yet to set them, or needs a default initial value, the preset values are what will be returned. +You need to set the standard dictionary in the config so that `FreqAI` can return proper dataframe shapes. These values will likely be overridden by the prediction model, but in the case where the model has yet to set them, or needs a default initial value, the pre-set values are what will be returned. ## Feature normalization @@ -265,4 +265,4 @@ 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) (external website)) with `min_samples` ($N$) taken as 1/4 of the no. of time points in the feature set. `eps` ($\varepsilon$) is computed automatically 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. +`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) (external website)) with `min_samples` ($N$) taken as 1/4 of the no. of time points (candles) in the feature set. `eps` ($\varepsilon$) is computed automatically 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. diff --git a/docs/freqai-parameter-table.md b/docs/freqai-parameter-table.md index 8e19226ba..2ec8fac30 100644 --- a/docs/freqai-parameter-table.md +++ b/docs/freqai-parameter-table.md @@ -11,8 +11,8 @@ Mandatory parameters are marked as **Required** and have to be set in one of the | `train_period_days` | **Required.**
Number of days to use for the training data (width of the sliding window).
**Datatype:** Positive integer. | `backtest_period_days` | **Required.**
Number of days to inference from the trained model before sliding the `train_period_days` window defined above, and retraining the model during backtesting (more info [here](freqai-running.md#backtesting)). This can be fractional days, but beware that the provided `timerange` will be divided by this number to yield the number of trainings necessary to complete the backtest.
**Datatype:** Float. | `identifier` | **Required.**
A unique ID for the current model. If models are saved to disk, the `identifier` allows for reloading specific pre-trained models/data.
**Datatype:** String. -| `live_retrain_hours` | Frequency of retraining during dry/live runs.
**Datatype:** Float > 0.
Default: 0 (models retrain as often as possible). -| `expiration_hours` | Avoid making predictions if a model is more than `expiration_hours` old.
**Datatype:** Positive integer.
Default: 0 (models never expire). +| `live_retrain_hours` | Frequency of retraining during dry/live runs.
**Datatype:** Float > 0.
Default: `0` (models retrain as often as possible). +| `expiration_hours` | Avoid making predictions if a model is more than `expiration_hours` old.
**Datatype:** Positive integer.
Default: `0` (models never expire). | `purge_old_models` | Delete obsolete models.
**Datatype:** Boolean.
Default: `False` (all historic models remain on disk). | `save_backtest_models` | Save models to disk when running backtesting. Backtesting operates most efficiently by saving the prediction data and reusing them directly for subsequent runs (when you wish to tune entry/exit parameters). Saving backtesting models to disk also allows to use the same model files for starting a dry/live instance with the same model `identifier`.
**Datatype:** Boolean.
Default: `False` (no models are saved). | `fit_live_predictions_candles` | Number of historical candles to use for computing target (label) statistics from prediction data, instead of from the training dataset (more information can be found [here](freqai-configuration.md#creating-a-dynamic-target-threshold)).
**Datatype:** Positive integer. @@ -25,16 +25,16 @@ Mandatory parameters are marked as **Required** and have to be set in one of the | `label_period_candles` | Number of candles into the future that the labels are created for. This is used in `populate_any_indicators` (see `templates/FreqaiExampleStrategy.py` for detailed usage). You can create custom labels and choose whether to make use of this parameter or not.
**Datatype:** Positive integer. | `include_shifted_candles` | Add features from previous candles to subsequent candles with the intent of adding historical information. If used, `FreqAI` will duplicate and shift all features from the `include_shifted_candles` previous candles so that the information is available for the subsequent candle.
**Datatype:** Positive integer. | `weight_factor` | Weight training data points according to their recency (see details [here](freqai-feature-engineering.md#weighting-features-for-temporal-importance)).
**Datatype:** Positive float (typically < 1). -| `indicator_max_period_candles` | **No longer used (#7325)**. Replaced by `startup_candle_count` which is set in the [strategy](freqai-configuration.md#building-a-freqai-strategy). `startup_candle_count` is timeframe independent and defines the maximum *period* used in `populate_any_indicators()` for indicator creation. `FreqAI` uses this parameter together with the maximum timeframe in `include_time_frames` to calculate how many data points to download such that the first data point does not include a NaN
**Datatype:** Positive integer. +| `indicator_max_period_candles` | **No longer used (#7325)**. Replaced by `startup_candle_count` which is set in the [strategy](freqai-configuration.md#building-a-freqai-strategy). `startup_candle_count` is timeframe independent and defines the maximum *period* used in `populate_any_indicators()` for indicator creation. `FreqAI` uses this parameter together with the maximum timeframe in `include_time_frames` to calculate how many data points to download such that the first data point does not include a NaN.
**Datatype:** Positive integer. | `indicator_periods_candles` | Time periods to calculate indicators for. The indicators are added to the base indicator dataset.
**Datatype:** List of positive integers. -| `principal_component_analysis` | Automatically reduce the dimensionality of the data set using Principal Component Analysis. See details about how it works [here](#reducing-data-dimensionality-with-principal-component-analysis)
**Datatype:** Boolean. defaults to `False`. -| `plot_feature_importances` | Create a feature importance plot for each model for the top/bottom `plot_feature_importances` number of features.
**Datatype:** Integer, defaults to `0`. +| `principal_component_analysis` | Automatically reduce the dimensionality of the data set using Principal Component Analysis. See details about how it works [here](#reducing-data-dimensionality-with-principal-component-analysis)
**Datatype:** Boolean.
Default: `False`. +| `plot_feature_importances` | Create a feature importance plot for each model for the top/bottom `plot_feature_importances` number of features.
**Datatype:** Integer.
Default: `0`. | `DI_threshold` | Activates the use of the Dissimilarity Index for outlier detection when set to > 0. See details about how it works [here](freqai-feature-engineering.md#identifying-outliers-with-the-dissimilarity-index-di).
**Datatype:** Positive float (typically < 1). | `use_SVM_to_remove_outliers` | Train a support vector machine to detect and remove outliers from the training dataset, as well as from incoming data points. See details about how it works [here](freqai-feature-engineering.md#identifying-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](freqai-feature-engineering.md#identifying-outliers-using-a-support-vector-machine-svm).
**Datatype:** Dictionary. | `use_DBSCAN_to_remove_outliers` | Cluster data using the DBSCAN algorithm to identify and remove outliers from training and prediction data. See details about how it works [here](freqai-feature-engineering.md#identifying-outliers-with-dbscan).
**Datatype:** Boolean. -| `inlier_metric_window` | If set, `FreqAI` adds an `inlier_metric` to the training feature set and set the lookback to be the `inlier_metric_window`, i.e., the number of previous time points to compare the current candle to. Details of how the `inlier_metric` is computed can be found [here](freqai-feature-engineering.md#inlier-metric).
**Datatype:** Integer.
Default: 0. -| `noise_standard_deviation` | If set, `FreqAI` adds noise to the training features with the aim of preventing overfitting. `FreqAI` generates random deviates from a gaussian distribution with a standard deviation of `noise_standard_deviation` and adds them to all data points. `noise_standard_deviation` should be kept relative to the normalized space, i.e., between -1 and 1. In other words, since data in `FreqAI` is always normalized to be between -1 and 1, `noise_standard_deviation: 0.05` would result in 32% of the data being randomly increased/decreased by more than 2.5% (i.e., the percent of data falling within the first standard deviation).
**Datatype:** Integer.
Default: 0. +| `inlier_metric_window` | If set, `FreqAI` adds an `inlier_metric` to the training feature set and set the lookback to be the `inlier_metric_window`, i.e., the number of previous time points to compare the current candle to. Details of how the `inlier_metric` is computed can be found [here](freqai-feature-engineering.md#inlier-metric).
**Datatype:** Integer.
Default: `0`. +| `noise_standard_deviation` | If set, `FreqAI` adds noise to the training features with the aim of preventing overfitting. `FreqAI` generates random deviates from a gaussian distribution with a standard deviation of `noise_standard_deviation` and adds them to all data points. `noise_standard_deviation` should be kept relative to the normalized space, i.e., between -1 and 1. In other words, since data in `FreqAI` is always normalized to be between -1 and 1, `noise_standard_deviation: 0.05` would result in 32% of the data being randomly increased/decreased by more than 2.5% (i.e., the percent of data falling within the first standard deviation).
**Datatype:** Integer.
Default: `0`. | `outlier_protection_percentage` | Enable to prevent outlier detection methods from discarding too much data. 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, i.e., the original dataset will be kept intact. If the outlier protection is triggered, no predictions will be made based on the training dataset.
**Datatype:** Float.
Default: `30`. | `reverse_train_test_order` | Split the feature dataset (see below) and use the latest data split for training 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, you should be careful to understand the unorthodox nature of this parameter before employing it.
**Datatype:** Boolean.
Default: `False` (no reversal). | | **Data split parameters** @@ -43,9 +43,9 @@ Mandatory parameters are marked as **Required** and have to be set in one of the | `shuffle` | Shuffle the training data points during training. Typically, to not remove the chronological order of data in time-series forecasting, this is set to `False`.
**Datatype:** Boolean.
Defaut: `False`. | | **Model training parameters** | `model_training_parameters` | A flexible dictionary that includes all parameters available by the selected model library. For example, if you use `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 you select 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_estimators` | The number of boosted trees to fit in the training of the model.
**Datatype:** Integer. +| `learning_rate` | Boosting learning rate during training of the model.
**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. | | **Extraneous parameters** | `keras` | If the selected model makes use of Keras (typical for Tensorflow-based prediction models), this flag needs to be activated so that the model save/loading follows Keras standards.
**Datatype:** Boolean.
Default: `False`. -| `conv_width` | The width of a convolutional neural network input tensor. This replaces the need for shifting candles (`include_shifted_candles`) by feeding in historical data points as the second dimension of the tensor. Technically, this parameter can also be used for regressors, but it only adds computational overhead and does not change the model training/prediction.
**Datatype:** Integer.
Default: 2. +| `conv_width` | The width of a convolutional neural network input tensor. This replaces the need for shifting candles (`include_shifted_candles`) by feeding in historical data points as the second dimension of the tensor. Technically, this parameter can also be used for regressors, but it only adds computational overhead and does not change the model training/prediction.
**Datatype:** Integer.
Default: `2`. diff --git a/docs/freqai-running.md b/docs/freqai-running.md index bfefe88c2..6299a80f3 100644 --- a/docs/freqai-running.md +++ b/docs/freqai-running.md @@ -1,4 +1,4 @@ -# Running FreqAI +# Running `FreqAI` There are two ways to train and deploy an adaptive machine learning model - live deployment and historical backtesting. In both cases, `FreqAI` runs/simulates periodic retraining of models as shown in the following figure: @@ -6,13 +6,13 @@ There are two ways to train and deploy an adaptive machine learning model - live ## Live deployments -FreqAI can be run dry/live using the following command: +`FreqAI` can be run dry/live using the following command: ```bash freqtrade trade --strategy FreqaiExampleStrategy --config config_freqai.example.json --freqaimodel LightGBMRegressor ``` -When launched, FreqAI will start training a new model, with a new `identifier`, based on the config settings. Following training, the model will be used to make predictions on incoming candles until a new model is available. New models are typically generated as often as possible, with FreqAI managing an internal queue of the coin pairs to try to keep all models equally up to date. FreqAI will always use the most recently trained model to make predictions on incoming live data. If you do not want FreqAI to retrain new models as often as possible, you can set `live_retrain_hours` to tell FreqAI to wait at least that number of hours before training a new model. Additionally, you can set `expired_hours` to tell FreqAI to avoid making predictions on models that are older than that number of hours. +When launched, `FreqAI` will start training a new model, with a new `identifier`, based on the config settings. Following training, the model will be used to make predictions on incoming candles until a new model is available. New models are typically generated as often as possible, with `FreqAI` managing an internal queue of the coin pairs to try to keep all models equally up to date. `FreqAI` will always use the most recently trained model to make predictions on incoming live data. If you do not want `FreqAI` to retrain new models as often as possible, you can set `live_retrain_hours` to tell `FreqAI` to wait at least that number of hours before training a new model. Additionally, you can set `expired_hours` to tell `FreqAI` to avoid making predictions on models that are older than that number of hours. Trained models are by default saved to disk to allow for reuse during backtesting or after a crash. You can opt to [purge old models](#purging-old-model-data) to save disk space by setting `"purge_old_models": true` in the config. @@ -25,19 +25,19 @@ To start a dry/live run from a saved backtest model (or from a previously crashe } ``` -In this case, although FreqAI will initiate with a pre-trained model, it will still check to see how much time has elapsed since the model was trained. If a full `live_retrain_hours` has elapsed since the end of the loaded model, FreqAI will start training a new model. +In this case, although `FreqAI` will initiate with a pre-trained model, it will still check to see how much time has elapsed since the model was trained. If a full `live_retrain_hours` has elapsed since the end of the loaded model, `FreqAI` will start training a new model. ### Automatic data download -FreqAI automatically downloads the proper amount of data needed to ensure training of a model through the defined `train_period_days` and `startup_candle_count` (see the [parameter table](freqai-parameter-table.md) for detailed descriptions of these parameters). +`FreqAI` automatically downloads the proper amount of data needed to ensure training of a model through the defined `train_period_days` and `startup_candle_count` (see the [parameter table](freqai-parameter-table.md) for detailed descriptions of these parameters). ### Saving prediction data -All predictions made during the lifetime of a specific `identifier` model are stored in `historical_predictions.pkl` to allow for reloading after a crash or changes made to the config. +All predictions made during the lifetime of a specific `identifier` model are stored in `historic_predictions.pkl` to allow for reloading after a crash or changes made to the config. ### Purging old model data -FreqAI stores new model files after each successful training. These files become obsolete as new models are generated to adapt to new market conditions. If you are planning to leave FreqAI running for extended periods of time with high frequency retraining, you should enable `purge_old_models` in the config: +`FreqAI` stores new model files after each successful training. These files become obsolete as new models are generated to adapt to new market conditions. If you are planning to leave `FreqAI` running for extended periods of time with high frequency retraining, you should enable `purge_old_models` in the config: ```json "freqai": { @@ -49,20 +49,20 @@ This will automatically purge all models older than the two most recently traine ## Backtesting -The FreqAI backtesting module can be executed with the following command: +The `FreqAI` backtesting module can be executed with the following command: ```bash freqtrade backtesting --strategy FreqaiExampleStrategy --strategy-path freqtrade/templates --config config_examples/config_freqai.example.json --freqaimodel LightGBMRegressor --timerange 20210501-20210701 ``` -If this command has never been executed with the existing config file, FreqAI will train a new model +If this command has never been executed with the existing config file, `FreqAI` will train a new model for each pair, for each backtesting window within the expanded `--timerange`. -Backtesting mode requires [downloading the necessary data](#downloading-data-to-cover-the-full-backtest-period) before deployment (unlike in dry/live mode where FreqAI handles the data downloading automatically). You 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 set backtesting time range. More details on how to calculate the data to download can be found [here](#deciding-the-size-of-the-sliding-training-window-and-backtesting-duration). +Backtesting mode requires [downloading the necessary data](#downloading-data-to-cover-the-full-backtest-period) before deployment (unlike in dry/live mode where `FreqAI` handles the data downloading automatically). You 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 set backtesting time range. More details on how to calculate the data to download can be found [here](#deciding-the-size-of-the-sliding-training-window-and-backtesting-duration). !!! Note "Model reuse" Once the training is completed, you can execute the backtesting again with the same config file and - FreqAI will find the trained models and load them instead of spending time training. This is useful + `FreqAI` will find the trained models and load them instead of spending time training. This is useful if you want to tweak (or even hyperopt) buy and sell criteria inside the strategy. If you *want* to retrain a new model with the same config file, you should simply change the `identifier`. This way, you can return to using any model you wish by simply specifying the `identifier`. @@ -71,7 +71,7 @@ Backtesting mode requires [downloading the necessary data](#downloading-data-to- ### Saving prediction data -To allow for tweaking your strategy (**not** the features!), FreqAI will automatically save the predictions during backtesting so that they can be reused for future backtests and live runs using the same `identifier` model. This provides a performance enhancement geared towards enabling **high-level hyperopting** of entry/exit criteria. +To allow for tweaking your strategy (**not** the features!), `FreqAI` will automatically save the predictions during backtesting so that they can be reused for future backtests and live runs using the same `identifier` model. This provides a performance enhancement geared towards enabling **high-level hyperopting** of entry/exit criteria. An additional directory called `predictions`, which contains all the predictions stored in `hdf` format, will be created in the `unique-id` folder. @@ -81,21 +81,21 @@ To save the models generated during a particular backtest so that you can start ### Downloading data to cover the full backtest period -For live/dry deployments, FreqAI will download the necessary data automatically. However, to use backtesting functionality, you need to download the necessary data using `download-data` (details [here](data-download.md#data-downloading)). You need to pay careful attention to understanding how much *additional* data needs to be downloaded to ensure that there is a sufficient amount of training data *before* the start of the 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` (see the [parameter table](freqai-parameter-table.md) for detailed descriptions of these parameters) from the beginning of the desired backtesting timerange. +For live/dry deployments, `FreqAI` will download the necessary data automatically. However, to use backtesting functionality, you need to download the necessary data using `download-data` (details [here](data-download.md#data-downloading)). You need to pay careful attention to understanding how much *additional* data needs to be downloaded to ensure that there is a sufficient amount of training data *before* the start of the backtesting time range. The amount of additional data can be roughly estimated by moving the start date of the time range backwards by `train_period_days` and the `startup_candle_count` (see the [parameter table](freqai-parameter-table.md) for detailed descriptions of these parameters) from the beginning of the desired backtesting time range. -As an example, to backtest the `--timerange 20210501-20210701` using the [example config](freqai-configuration.md#setting-up-the-configuration-file) which sets `train_period_days` to 30, together with `startup_candle_count: 40` on a maximum `include_timeframes` of 1h, the start date for the downloaded data needs to be `20210501` - 30 days - 40 * 1h / 24 hours = 20210330 (31.7 days earlier than the start of the desired training timerange). +As an example, to backtest the `--timerange 20210501-20210701` using the [example config](freqai-configuration.md#setting-up-the-configuration-file) which sets `train_period_days` to 30, together with `startup_candle_count: 40` on a maximum `include_timeframes` of 1h, the start date for the downloaded data needs to be `20210501` - 30 days - 40 * 1h / 24 hours = 20210330 (31.7 days earlier than the start of the desired training time range). ### Deciding the size of the sliding training window and backtesting duration -The backtesting timerange is defined with the typical `--timerange` parameter in the configuration file. The duration of the sliding training window is set by `train_period_days`, whilst `backtest_period_days` is the sliding backtesting window, both in number of days (`backtest_period_days` can be -a float to indicate sub-daily retraining in live/dry mode). In the presented [example config](freqai-configuration.md#setting-up-the-configuration-file) (found in `config_examples/config_freqai.example.json`), the user is asking FreqAI to use a training period of 30 days and backtest on the subsequent 7 days. After the training of the model, FreqAI will backtest the subsequent 7 days. The "sliding window" then moves one week forward (emulating FreqAI retraining once per week in live mode) and the new model uses the previous 30 days (including the 7 days used for backtesting by the previous model) to train. This is repeated until the end of `--timerange`. This means that if you set `--timerange 20210501-20210701`, FreqAI will have trained 8 separate models at the end of `--timerange` (because the full range comprises 8 weeks). +The backtesting time range is defined with the typical `--timerange` parameter in the configuration file. The duration of the sliding training window is set by `train_period_days`, whilst `backtest_period_days` is the sliding backtesting window, both in number of days (`backtest_period_days` can be +a float to indicate sub-daily retraining in live/dry mode). In the presented [example config](freqai-configuration.md#setting-up-the-configuration-file) (found in `config_examples/config_freqai.example.json`), the user is asking `FreqAI` to use a training period of 30 days and backtest on the subsequent 7 days. After the training of the model, `FreqAI` will backtest the subsequent 7 days. The "sliding window" then moves one week forward (emulating `FreqAI` retraining once per week in live mode) and the new model uses the previous 30 days (including the 7 days used for backtesting by the previous model) to train. This is repeated until the end of `--timerange`. This means that if you set `--timerange 20210501-20210701`, `FreqAI` will have trained 8 separate models at the end of `--timerange` (because the full range comprises 8 weeks). !!! Note - Although fractional `backtest_period_days` is allowed, you 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, by setting a `--timerange` of 10 days, and 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 train constantly. In this case, backtesting would take the exact same amount of time as a dry run. + Although fractional `backtest_period_days` is allowed, you 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, by setting a `--timerange` of 10 days, and 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 train constantly. In this case, backtesting would take the exact same amount of time as a dry run. ## Defining model expirations -During dry/live mode, FreqAI trains each coin pair sequentially (on separate threads/GPU from the main Freqtrade bot). This means that there is always an age discrepancy between models. If you are training on 50 pairs, and each pair requires 5 minutes to train, the oldest model will be over 4 hours old. This may be undesirable if the characteristic time scale (the trade duration target) for a strategy is less than 4 hours. You can decide to only make trade entries if the model is less than a certain number of hours old by setting the `expiration_hours` in the config file: +During dry/live mode, `FreqAI` trains each coin pair sequentially (on separate threads/GPU from the main Freqtrade bot). This means that there is always an age discrepancy between models. If you are training on 50 pairs, and each pair requires 5 minutes to train, the oldest model will be over 4 hours old. This may be undesirable if the characteristic time scale (the trade duration target) for a strategy is less than 4 hours. You can decide to only make trade entries if the model is less than a certain number of hours old by setting the `expiration_hours` in the config file: ```json "freqai": { @@ -107,15 +107,15 @@ In the presented example config, the user will only allow predictions on models ## Controlling the model learning process -Model training parameters are unique to the selected machine learning library. FreqAI allows you to set any parameter for any library using the `model_training_parameters` dictionary in the config. The example config (found in `config_examples/config_freqai.example.json`) shows some of the example parameters associated with `Catboost` and `LightGBM`, but you can add any parameters available in those libraries or any other machine learning library you choose to implement. +Model training parameters are unique to the selected machine learning library. `FreqAI` allows you to set any parameter for any library using the `model_training_parameters` dictionary in the config. The example config (found in `config_examples/config_freqai.example.json`) shows some of the example parameters associated with `Catboost` and `LightGBM`, but you can add any parameters available in those libraries or any other machine learning library you choose to implement. Data split parameters are defined in `data_split_parameters` which can be any parameters associated with Scikit-learn's `train_test_split()` function. `train_test_split()` has a parameters called `shuffle` which allows to shuffle the data or keep it unshuffled. This is particularly useful to avoid biasing training with temporally auto-correlated data. More details about these parameters can be found the [Scikit-learn website](https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.train_test_split.html) (external website). -The FreqAI specific parameter `label_period_candles` defines the offset (number of candles into the future) used for the `labels`. In the presented [example config](freqai-configuration.md#setting-up-the-configuration-file), the user is asking for `labels` that are 24 candles in the future. +The `FreqAI` specific parameter `label_period_candles` defines the offset (number of candles into the future) used for the `labels`. In the presented [example config](freqai-configuration.md#setting-up-the-configuration-file), the user is asking for `labels` that are 24 candles in the future. ## Continual learning -You can choose to adopt a continual learning scheme by setting `"continual_learning": true` in the config. By enabling `continual_learning`, after training an initial model from scratch, subsequent trainings will start from the final model state of the preceding training. This gives the new model a "memory" of the previous state. By default, this is set to `false` which means that all new models are trained from scratch, without input from previous models. +You can choose to adopt a continual learning scheme by setting `"continual_learning": true` in the config. By enabling `continual_learning`, after training an initial model from scratch, subsequent trainings will start from the final model state of the preceding training. This gives the new model a "memory" of the previous state. By default, this is set to `False` which means that all new models are trained from scratch, without input from previous models. ## Hyperopt @@ -125,15 +125,15 @@ You can hyperopt using the same command as for [typical Freqtrade hyperopt](hype freqtrade hyperopt --hyperopt-loss SharpeHyperOptLoss --strategy FreqaiExampleStrategy --freqaimodel LightGBMRegressor --strategy-path freqtrade/templates --config config_examples/config_freqai.example.json --timerange 20220428-20220507 ``` -`hyperopt` requires you to have the data pre-downloaded in the same fashion as if you were doing [backtesting](#backtesting). In addition, you must consider some restrictions when trying to hyperopt FreqAI strategies: +`hyperopt` requires you to have the data pre-downloaded in the same fashion as if you were doing [backtesting](#backtesting). In addition, you must consider some restrictions when trying to hyperopt `FreqAI` strategies: -- The `--analyze-per-epoch` hyperopt parameter is not compatible with FreqAI. +- The `--analyze-per-epoch` hyperopt parameter is not compatible with `FreqAI`. - It's not possible to hyperopt indicators in the `populate_any_indicators()` function. This means that you 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 instructions also apply to hyperopt. -The best method for combining hyperopt and FreqAI is to focus on hyperopting entry/exit thresholds/criteria. You need to focus on hyperopting parameters that are not used in your features. For example, you should not try to hyperopt rolling window lengths in the feature creation, or any part of the 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. +The best method for combining hyperopt and `FreqAI` is to focus on hyperopting entry/exit thresholds/criteria. You need to focus on hyperopting parameters that are not used in your features. For example, you should not try to hyperopt rolling window lengths in the feature creation, or any part of the `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 threshold for the [Dissimilarity Index (DI)](freqai-feature-engineering.md#identifying-outliers-with-the-dissimilarity-index-di) `DI_values` beyond which we consider data points as outliers: +A good example of a hyperoptable parameter in `FreqAI` is a threshold for the [Dissimilarity Index (DI)](freqai-feature-engineering.md#identifying-outliers-with-the-dissimilarity-index-di) `DI_values` beyond which we consider data points as outliers: ```python di_max = IntParameter(low=1, high=20, default=10, space='buy', optimize=True, load=True) diff --git a/docs/freqai.md b/docs/freqai.md index 91adbf7ef..5ab019ca7 100644 --- a/docs/freqai.md +++ b/docs/freqai.md @@ -45,17 +45,17 @@ An overview of the algorithm, explaining the data processing pipeline and model ### Important machine learning vocabulary -**Features** - the parameters, based on historic data, on which a model is trained. All features for a single candle is stored as a vector. In `FreqAI`, you build a feature data sets from anything you can construct in the strategy. +**Features** - the parameters, based on historic data, on which a model is trained. All features for a single candle are stored as a vector. In `FreqAI`, you build a feature data set from anything you can construct in the strategy. -**Labels** - the target values that a model is trained toward. Each feature vector is associated with a single label that is defined by you within the strategy. These labels intentionally look into the future, and are not available to the model during dry/live/backtesting. +**Labels** - the target values that the model is trained toward. Each feature vector is associated with a single label that is defined by you within the strategy. These labels intentionally look into the future and are what you are training the model to be able to predict. -**Training** - the process of "teaching" the model to match the feature sets to the associated labels. Different types of models "learn" in different ways. More information about the different models can be found [here](freqai-configuration.md#using-different-prediction-models). +**Training** - the process of "teaching" the model to match the feature sets to the associated labels. Different types of models "learn" in different ways which means that one might be better than another for a specific application. More information about the different models that are already implemented in `FreqAI` can be found [here](freqai-configuration.md#using-different-prediction-models). -**Train data** - a subset of the feature data set that is fed to the model during training. This data directly influences weight connections in the model. +**Train data** - a subset of the feature data set that is fed to the model during training to "teach" the model how to predict the targets. This data directly influences weight connections in the model. **Test data** - a subset of the feature data set that is used to evaluate the performance of the model after training. This data does not influence nodal weights within the model. -**Inferencing** - the process of feeding a trained model new data on which it will make a prediction. +**Inferencing** - the process of feeding a trained model new unseen data on which it will make a prediction. ## Install prerequisites @@ -96,5 +96,4 @@ Software development: Wagner Costa @wagnercosta Beta testing and bug reporting: -Stefan Gehring @bloodhunter4rc, @longyu, Andrew Robert Lawless @paranoidandy, Pascal Schmidt @smidelis, Ryan McMullan @smarmau, -Juha Nykänen @suikula, Johan van der Vlugt @jooopiert, Richárd Józsa @richardjosza +Stefan Gehring @bloodhunter4rc, @longyu, Andrew Lawless @paranoidandy, Pascal Schmidt @smidelis, Ryan McMullan @smarmau, Juha Nykänen @suikula, Johan van der Vlugt @jooopiert, Richárd Józsa @richardjosza, Timothy Pogue @wizrds From 1727f99b58906420bd656f4ef08162ab98500d58 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 2 Oct 2022 18:11:52 +0200 Subject: [PATCH 073/126] Fix missing mock --- tests/test_freqtradebot.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 462857dd6..cdea772dc 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -3095,6 +3095,9 @@ def test_handle_cancel_enter_corder_empty(mocker, default_conf_usdt, limit_order cancel_order_mock.reset_mock() l_order['filled'] = 1.0 + order = deepcopy(l_order) + order['status'] = 'canceled' + mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=order) assert not freqtrade.handle_cancel_enter(trade, l_order, reason) assert cancel_order_mock.call_count == 1 From b70f18f4c36c13aa3930fa812148fd452a48e18b Mon Sep 17 00:00:00 2001 From: robcaulk Date: Sun, 2 Oct 2022 18:33:39 +0200 Subject: [PATCH 074/126] add close price and date to historic_predictions --- freqtrade/freqai/data_drawer.py | 6 +++++- freqtrade/freqai/freqai_interface.py | 9 ++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/freqtrade/freqai/data_drawer.py b/freqtrade/freqai/data_drawer.py index 471f6875c..0d3bdea29 100644 --- a/freqtrade/freqai/data_drawer.py +++ b/freqtrade/freqai/data_drawer.py @@ -257,7 +257,7 @@ class FreqaiDataDrawer: def append_model_predictions(self, pair: str, predictions: DataFrame, do_preds: NDArray[np.int_], - dk: FreqaiDataKitchen, len_df: int) -> None: + dk: FreqaiDataKitchen, strat_df: DataFrame) -> None: """ Append model predictions to historic predictions dataframe, then set the strategy return dataframe to the tail of the historic predictions. The length of @@ -266,6 +266,7 @@ class FreqaiDataDrawer: historic predictions. """ + len_df = len(strat_df) index = self.historic_predictions[pair].index[-1:] columns = self.historic_predictions[pair].columns @@ -293,6 +294,9 @@ class FreqaiDataDrawer: for return_str in rets: df[return_str].iloc[-1] = rets[return_str] + df['close_price'].iloc[-1] = strat_df['close'].iloc[-1] + df['date_pred'].iloc[-1] = strat_df['date'].iloc[-1] + self.model_return_values[pair] = df.tail(len_df).reset_index(drop=True) def attach_return_values_to_return_dataframe( diff --git a/freqtrade/freqai/freqai_interface.py b/freqtrade/freqai/freqai_interface.py index 78539bae5..5ac7bc32c 100644 --- a/freqtrade/freqai/freqai_interface.py +++ b/freqtrade/freqai/freqai_interface.py @@ -393,7 +393,7 @@ class IFreqaiModel(ABC): # allows FreqUI to show full return values. pred_df, do_preds = self.predict(dataframe, dk) if pair not in self.dd.historic_predictions: - self.set_initial_historic_predictions(pred_df, dk, pair) + self.set_initial_historic_predictions(pred_df, dk, pair, dataframe) self.dd.set_initial_return_values(pair, pred_df) dk.return_dataframe = self.dd.attach_return_values_to_return_dataframe(pair, dataframe) @@ -414,7 +414,7 @@ class IFreqaiModel(ABC): if self.freqai_info.get('fit_live_predictions_candles', 0) and self.live: self.fit_live_predictions(dk, pair) - self.dd.append_model_predictions(pair, pred_df, do_preds, dk, len(dataframe)) + self.dd.append_model_predictions(pair, pred_df, do_preds, dk, dataframe) dk.return_dataframe = self.dd.attach_return_values_to_return_dataframe(pair, dataframe) return @@ -583,7 +583,7 @@ class IFreqaiModel(ABC): self.dd.purge_old_models() def set_initial_historic_predictions( - self, pred_df: DataFrame, dk: FreqaiDataKitchen, pair: str + self, pred_df: DataFrame, dk: FreqaiDataKitchen, pair: str, strat_df: DataFrame ) -> None: """ This function is called only if the datadrawer failed to load an @@ -626,6 +626,9 @@ class IFreqaiModel(ABC): for return_str in dk.data['extra_returns_per_train']: hist_preds_df[return_str] = 0 + hist_preds_df['close_price'] = strat_df['close'] + hist_preds_df['date_pred'] = strat_df['date'] + # # for keras type models, the conv_window needs to be prepended so # # viewing is correct in frequi if self.freqai_info.get('keras', False) or self.ft_params.get('inlier_metric_window', 0): From 6f7b75d4b064b1e6803fdf803520d4cb30acc26f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Oct 2022 03:01:13 +0000 Subject: [PATCH 075/126] Bump time-machine from 2.8.1 to 2.8.2 Bumps [time-machine](https://github.com/adamchainz/time-machine) from 2.8.1 to 2.8.2. - [Release notes](https://github.com/adamchainz/time-machine/releases) - [Changelog](https://github.com/adamchainz/time-machine/blob/main/HISTORY.rst) - [Commits](https://github.com/adamchainz/time-machine/compare/2.8.1...2.8.2) --- updated-dependencies: - dependency-name: time-machine 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 d50105662..5b76a4701 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -17,7 +17,7 @@ pytest-mock==3.8.2 pytest-random-order==1.0.4 isort==5.10.1 # For datetime mocking -time-machine==2.8.1 +time-machine==2.8.2 # Convert jupyter notebooks to markdown documents nbconvert==7.0.0 From f722104f7e6c314f40a876317662fc2bab77ee87 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Oct 2022 03:01:30 +0000 Subject: [PATCH 076/126] Bump catboost from 1.0.6 to 1.1 Bumps [catboost](https://github.com/catboost/catboost) from 1.0.6 to 1.1. - [Release notes](https://github.com/catboost/catboost/releases) - [Changelog](https://github.com/catboost/catboost/blob/master/RELEASE.md) - [Commits](https://github.com/catboost/catboost/compare/v1.0.6...v1.1) --- updated-dependencies: - dependency-name: catboost dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements-freqai.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-freqai.txt b/requirements-freqai.txt index 9cdd431fe..cf0d2eb07 100644 --- a/requirements-freqai.txt +++ b/requirements-freqai.txt @@ -4,6 +4,6 @@ # Required for freqai scikit-learn==1.1.2 joblib==1.2.0 -catboost==1.0.6; platform_machine != 'aarch64' +catboost==1.1; platform_machine != 'aarch64' lightgbm==3.3.2 xgboost==1.6.2 From 0a7e4d6da5cfca90503c29d082153e243276c746 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Oct 2022 03:01:36 +0000 Subject: [PATCH 077/126] Bump mkdocs from 1.3.1 to 1.4.0 Bumps [mkdocs](https://github.com/mkdocs/mkdocs) from 1.3.1 to 1.4.0. - [Release notes](https://github.com/mkdocs/mkdocs/releases) - [Commits](https://github.com/mkdocs/mkdocs/compare/1.3.1...1.4.0) --- updated-dependencies: - dependency-name: mkdocs dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- docs/requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index 176947438..6e199f8b4 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,5 +1,5 @@ markdown==3.3.7 -mkdocs==1.3.1 +mkdocs==1.4.0 mkdocs-material==8.5.3 mdx_truly_sane_lists==1.3 pymdown-extensions==9.5 From 70d6c27e3efb6499b0870a3f0f05094d3cea04f2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Oct 2022 03:01:38 +0000 Subject: [PATCH 078/126] Bump pytest-mock from 3.8.2 to 3.9.0 Bumps [pytest-mock](https://github.com/pytest-dev/pytest-mock) from 3.8.2 to 3.9.0. - [Release notes](https://github.com/pytest-dev/pytest-mock/releases) - [Changelog](https://github.com/pytest-dev/pytest-mock/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest-mock/compare/v3.8.2...v3.9.0) --- updated-dependencies: - dependency-name: pytest-mock dependency-type: direct:development update-type: version-update:semver-minor ... 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 d50105662..788df662d 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -13,7 +13,7 @@ pre-commit==2.20.0 pytest==7.1.3 pytest-asyncio==0.19.0 pytest-cov==3.0.0 -pytest-mock==3.8.2 +pytest-mock==3.9.0 pytest-random-order==1.0.4 isort==5.10.1 # For datetime mocking From 373132e135adedb4f919e30303f50751fb0d389c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Oct 2022 03:01:50 +0000 Subject: [PATCH 079/126] Bump ccxt from 1.93.98 to 1.95.2 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.93.98 to 1.95.2. - [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.93.98...1.95.2) --- 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 366b3c3fa..3cc830290 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ pandas==1.5.0; platform_machine != 'armv7l' pandas==1.4.3; platform_machine == 'armv7l' pandas-ta==0.3.14b -ccxt==1.93.98 +ccxt==1.95.2 # Pin cryptography for now due to rust build errors with piwheels cryptography==38.0.1 aiohttp==3.8.3 From 6defa62297302c7c2dea43363c46633c3cb0fa1d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Oct 2022 04:35:31 +0000 Subject: [PATCH 080/126] Bump mkdocs-material from 8.5.3 to 8.5.6 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 8.5.3 to 8.5.6. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/8.5.3...8.5.6) --- updated-dependencies: - dependency-name: mkdocs-material dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- docs/requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index 6e199f8b4..b5548aeea 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,6 +1,6 @@ markdown==3.3.7 mkdocs==1.4.0 -mkdocs-material==8.5.3 +mkdocs-material==8.5.6 mdx_truly_sane_lists==1.3 pymdown-extensions==9.5 jinja2==3.1.2 From f3d4c56b3bead774805f57f76cc3c05805a57caf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Oct 2022 04:35:34 +0000 Subject: [PATCH 081/126] Bump pytest-cov from 3.0.0 to 4.0.0 Bumps [pytest-cov](https://github.com/pytest-dev/pytest-cov) from 3.0.0 to 4.0.0. - [Release notes](https://github.com/pytest-dev/pytest-cov/releases) - [Changelog](https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest-cov/compare/v3.0.0...v4.0.0) --- updated-dependencies: - dependency-name: pytest-cov dependency-type: direct:development update-type: version-update:semver-major ... 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 788df662d..fcb364930 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -12,7 +12,7 @@ mypy==0.971 pre-commit==2.20.0 pytest==7.1.3 pytest-asyncio==0.19.0 -pytest-cov==3.0.0 +pytest-cov==4.0.0 pytest-mock==3.9.0 pytest-random-order==1.0.4 isort==5.10.1 From 4cf4642a6caa4f06d558cacd48a5cf31337cdfcf Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 3 Oct 2022 06:39:20 +0200 Subject: [PATCH 082/126] Parametrize EMC test --- tests/rpc/test_rpc_emc.py | 38 +++++++------------------------------- 1 file changed, 7 insertions(+), 31 deletions(-) diff --git a/tests/rpc/test_rpc_emc.py b/tests/rpc/test_rpc_emc.py index 84a2658a0..93ae829d5 100644 --- a/tests/rpc/test_rpc_emc.py +++ b/tests/rpc/test_rpc_emc.py @@ -188,15 +188,19 @@ async def test_emc_create_connection_success(default_conf, caplog, mocker): emc.shutdown() -async def test_emc_create_connection_invalid_port(default_conf, caplog, mocker): +@pytest.mark.parametrize('host,port', [ + (_TEST_WS_HOST, -1), + ("10000.1241..2121/", _TEST_WS_PORT), +]) +async def test_emc_create_connection_invalid_url(default_conf, caplog, mocker, host, port): default_conf.update({ "external_message_consumer": { "enabled": True, "producers": [ { "name": "default", - "host": _TEST_WS_HOST, - "port": -1, + "host": host, + "port": port, "ws_token": _TEST_WS_TOKEN } ], @@ -219,34 +223,6 @@ async def test_emc_create_connection_invalid_port(default_conf, caplog, mocker): emc.shutdown() -async def test_emc_create_connection_invalid_host(default_conf, caplog, mocker): - default_conf.update({ - "external_message_consumer": { - "enabled": True, - "producers": [ - { - "name": "default", - "host": "10000.1241..2121/", - "port": _TEST_WS_PORT, - "ws_token": _TEST_WS_TOKEN - } - ], - "wait_timeout": 60, - "ping_timeout": 60, - "sleep_timeout": 60 - } - }) - - dp = DataProvider(default_conf, None, None, None) - emc = ExternalMessageConsumer(default_conf, dp) - - try: - await asyncio.sleep(0.01) - assert log_has_re(r".+ is an invalid WebSocket URL .+", caplog) - finally: - emc.shutdown() - - async def test_emc_create_connection_error(default_conf, caplog, mocker): default_conf.update({ "external_message_consumer": { From 3c789bca63420b35de233a8b4ec0fe7de03d024a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Oct 2022 05:07:25 +0000 Subject: [PATCH 083/126] Bump pymdown-extensions from 9.5 to 9.6 Bumps [pymdown-extensions](https://github.com/facelessuser/pymdown-extensions) from 9.5 to 9.6. - [Release notes](https://github.com/facelessuser/pymdown-extensions/releases) - [Commits](https://github.com/facelessuser/pymdown-extensions/compare/9.5...9.6) --- updated-dependencies: - dependency-name: pymdown-extensions dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- docs/requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index b5548aeea..4ff1780cf 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -2,5 +2,5 @@ markdown==3.3.7 mkdocs==1.4.0 mkdocs-material==8.5.6 mdx_truly_sane_lists==1.3 -pymdown-extensions==9.5 +pymdown-extensions==9.6 jinja2==3.1.2 From a78d6a05a6477d16c6a983befc2c3599b7537e61 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Oct 2022 06:10:30 +0000 Subject: [PATCH 084/126] Bump mypy from 0.971 to 0.981 Bumps [mypy](https://github.com/python/mypy) from 0.971 to 0.981. - [Release notes](https://github.com/python/mypy/releases) - [Commits](https://github.com/python/mypy/compare/v0.971...v0.981) --- updated-dependencies: - dependency-name: mypy dependency-type: direct:development update-type: version-update:semver-minor ... 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 fcb364930..922f6980d 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -8,7 +8,7 @@ coveralls==3.3.1 flake8==5.0.4 flake8-tidy-imports==4.8.0 -mypy==0.971 +mypy==0.981 pre-commit==2.20.0 pytest==7.1.3 pytest-asyncio==0.19.0 From 6ecd92de4a8cdcccd364aa56098b3b4ba89f93de Mon Sep 17 00:00:00 2001 From: Robert Caulk Date: Mon, 3 Oct 2022 09:55:57 +0200 Subject: [PATCH 085/126] Allow updating without changing identifier --- freqtrade/freqai/data_drawer.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/freqtrade/freqai/data_drawer.py b/freqtrade/freqai/data_drawer.py index 0d3bdea29..603c477a0 100644 --- a/freqtrade/freqai/data_drawer.py +++ b/freqtrade/freqai/data_drawer.py @@ -294,6 +294,12 @@ class FreqaiDataDrawer: for return_str in rets: df[return_str].iloc[-1] = rets[return_str] + # this logic carries users between version without needing to + # change their identifier + if 'close_price' not in df.columns: + df['close_price'] = 0 + df['date_pred'] = 0 + df['close_price'].iloc[-1] = strat_df['close'].iloc[-1] df['date_pred'].iloc[-1] = strat_df['date'].iloc[-1] From c2d0eca9d896d70b62dfa436ac62615d02bc9325 Mon Sep 17 00:00:00 2001 From: th0rntwig Date: Mon, 3 Oct 2022 11:01:58 +0200 Subject: [PATCH 086/126] Remove backticks around FreqAI --- docs/freqai-configuration.md | 28 +++++++++--------- docs/freqai-developers.md | 8 +++--- docs/freqai-feature-engineering.md | 32 ++++++++++----------- docs/freqai-parameter-table.md | 16 +++++------ docs/freqai-running.md | 46 +++++++++++++++--------------- docs/freqai.md | 26 ++++++++--------- 6 files changed, 78 insertions(+), 78 deletions(-) diff --git a/docs/freqai-configuration.md b/docs/freqai-configuration.md index 683fc9b34..d24c60057 100644 --- a/docs/freqai-configuration.md +++ b/docs/freqai-configuration.md @@ -1,10 +1,10 @@ # Configuration -`FreqAI` is configured through the typical [Freqtrade config file](configuration.md) and the standard [Freqtrade strategy](strategy-customization.md). Examples of `FreqAI` config and strategy files can be found in `config_examples/config_freqai.example.json` and `freqtrade/templates/FreqaiExampleStrategy.py`, respectively. +FreqAI is configured through the typical [Freqtrade config file](configuration.md) and the standard [Freqtrade strategy](strategy-customization.md). Examples of FreqAI config and strategy files can be found in `config_examples/config_freqai.example.json` and `freqtrade/templates/FreqaiExampleStrategy.py`, respectively. ## Setting up the configuration file - Although there are plenty of additional parameters to choose from, as highlighted in the [parameter table](freqai-parameter-table.md#parameter-table), a `FreqAI` config must at minimum include the following parameters (the parameter values are only examples): + Although there are plenty of additional parameters to choose from, as highlighted in the [parameter table](freqai-parameter-table.md#parameter-table), a FreqAI config must at minimum include the following parameters (the parameter values are only examples): ```json "freqai": { @@ -35,9 +35,9 @@ A full example config is available in `config_examples/config_freqai.example.json`. -## Building a `FreqAI` strategy +## Building a FreqAI strategy -The `FreqAI` strategy requires including the following lines of code in the standard [Freqtrade strategy](strategy-customization.md): +The FreqAI strategy requires including the following lines of code in the standard [Freqtrade strategy](strategy-customization.md): ```python # user should define the maximum startup candle count (the largest number of candles @@ -129,7 +129,7 @@ Notice also the location of the labels under `if set_generalized_indicators:` at The `self.freqai.start()` function cannot be called outside the `populate_indicators()`. !!! Note - Features **must** be defined in `populate_any_indicators()`. Defining `FreqAI` features in `populate_indicators()` + Features **must** be defined in `populate_any_indicators()`. Defining FreqAI features in `populate_indicators()` will cause the algorithm to fail in live/dry mode. In order to add generalized features that are not associated with a specific pair or timeframe, the following structure inside `populate_any_indicators()` should be used (as exemplified in `freqtrade/templates/FreqaiExampleStrategy.py`): @@ -166,15 +166,15 @@ Below are the values you can expect to include/use inside a typical strategy dat | DataFrame Key | Description | |------------|-------------| -| `df['&*']` | Any dataframe column prepended with `&` in `populate_any_indicators()` is treated as a training target (label) inside `FreqAI` (typically following the naming convention `&-s*`). For example, to predict the close price 40 candles into the future, you would set `df['&-s_close'] = df['close'].shift(-self.freqai_info["feature_parameters"]["label_period_candles"])` with `"label_period_candles": 40` in the config. `FreqAI` makes the predictions and gives them back under the same key (`df['&-s_close']`) to be used in `populate_entry/exit_trend()`.
**Datatype:** Depends on the output of the model. +| `df['&*']` | Any dataframe column prepended with `&` in `populate_any_indicators()` is treated as a training target (label) inside FreqAI (typically following the naming convention `&-s*`). For example, to predict the close price 40 candles into the future, you would set `df['&-s_close'] = df['close'].shift(-self.freqai_info["feature_parameters"]["label_period_candles"])` with `"label_period_candles": 40` in the config. FreqAI makes the predictions and gives them back under the same key (`df['&-s_close']`) to be used in `populate_entry/exit_trend()`.
**Datatype:** Depends on the output of the model. | `df['&*_std/mean']` | Standard deviation and mean values of the defined labels during training (or live tracking with `fit_live_predictions_candles`). Commonly used to understand the rarity of a prediction (use the z-score as shown in `templates/FreqaiExampleStrategy.py` and explained [here](#creating-a-dynamic-target-threshold) to evaluate how often a particular prediction was observed during training or historically with `fit_live_predictions_candles`).
**Datatype:** Float. -| `df['do_predict']` | Indication of an outlier data point. The return value is integer between -2 and 2, which lets you know if the prediction is trustworthy or not. `do_predict==1` means that the prediction is trustworthy. If the Dissimilarity Index (DI, see details [here](freqai-feature-engineering.md#identifying-outliers-with-the-dissimilarity-index-di)) of the input data point is above the threshold defined in the config, `FreqAI` will subtract 1 from `do_predict`, resulting in `do_predict==0`. If `use_SVM_to_remove_outliers()` is active, the Support Vector Machine (SVM, see details [here](freqai-feature-engineering.md#identifying-outliers-using-a-support-vector-machine-svm)) may also detect outliers in training and prediction data. In this case, the SVM will also subtract 1 from `do_predict`. If the input data point was considered an outlier by the SVM but not by the DI, or vice versa, the result will be `do_predict==0`. If both the DI and the SVM considers the input data point to be an outlier, the result will be `do_predict==-1`. As with the SVM, if `use_DBSCAN_to_remove_outliers` is active, DBSCAN (see details [here](freqai-feature-engineering.md#identifying-outliers-with-dbscan)) may also detect outliers and subtract 1 from `do_predict`. Hence, if both the SVM and DBSCAN are active and identify a datapoint that was above the DI threshold as an outlier, the result will be `do_predict==-2`. A particular case is when `do_predict == 2`, which means that the model has expired due to exceeding `expired_hours`.
**Datatype:** Integer between -2 and 2. -| `df['DI_values']` | Dissimilarity Index (DI) values are proxies for the level of confidence `FreqAI` has in the prediction. A lower DI means the prediction is close to the training data, i.e., higher prediction confidence. See details about the DI [here](freqai-feature-engineering.md#identifying-outliers-with-the-dissimilarity-index-di).
**Datatype:** Float. -| `df['%*']` | Any dataframe column prepended with `%` in `populate_any_indicators()` is treated as a training feature. For example, you can include the RSI in the training feature set (similar to in `templates/FreqaiExampleStrategy.py`) by setting `df['%-rsi']`. See more details on how this is done [here](freqai-feature-engineering.md).
**Note:** Since the number of features prepended with `%` can multiply very quickly (10s of thousands of features are easily engineered using the multiplictative functionality of, e.g., `include_shifted_candles` and `include_timeframes` as described in the [parameter table](freqai-parameter-table.md)), these features are removed from the dataframe that is returned from `FreqAI` to the strategy. To keep a particular type of feature for plotting purposes, you would prepend it with `%%`.
**Datatype:** Depends on the output of the model. +| `df['do_predict']` | Indication of an outlier data point. The return value is integer between -2 and 2, which lets you know if the prediction is trustworthy or not. `do_predict==1` means that the prediction is trustworthy. If the Dissimilarity Index (DI, see details [here](freqai-feature-engineering.md#identifying-outliers-with-the-dissimilarity-index-di)) of the input data point is above the threshold defined in the config, FreqAI will subtract 1 from `do_predict`, resulting in `do_predict==0`. If `use_SVM_to_remove_outliers()` is active, the Support Vector Machine (SVM, see details [here](freqai-feature-engineering.md#identifying-outliers-using-a-support-vector-machine-svm)) may also detect outliers in training and prediction data. In this case, the SVM will also subtract 1 from `do_predict`. If the input data point was considered an outlier by the SVM but not by the DI, or vice versa, the result will be `do_predict==0`. If both the DI and the SVM considers the input data point to be an outlier, the result will be `do_predict==-1`. As with the SVM, if `use_DBSCAN_to_remove_outliers` is active, DBSCAN (see details [here](freqai-feature-engineering.md#identifying-outliers-with-dbscan)) may also detect outliers and subtract 1 from `do_predict`. Hence, if both the SVM and DBSCAN are active and identify a datapoint that was above the DI threshold as an outlier, the result will be `do_predict==-2`. A particular case is when `do_predict == 2`, which means that the model has expired due to exceeding `expired_hours`.
**Datatype:** Integer between -2 and 2. +| `df['DI_values']` | Dissimilarity Index (DI) values are proxies for the level of confidence FreqAI has in the prediction. A lower DI means the prediction is close to the training data, i.e., higher prediction confidence. See details about the DI [here](freqai-feature-engineering.md#identifying-outliers-with-the-dissimilarity-index-di).
**Datatype:** Float. +| `df['%*']` | Any dataframe column prepended with `%` in `populate_any_indicators()` is treated as a training feature. For example, you can include the RSI in the training feature set (similar to in `templates/FreqaiExampleStrategy.py`) by setting `df['%-rsi']`. See more details on how this is done [here](freqai-feature-engineering.md).
**Note:** Since the number of features prepended with `%` can multiply very quickly (10s of thousands of features are easily engineered using the multiplictative functionality of, e.g., `include_shifted_candles` and `include_timeframes` as described in the [parameter table](freqai-parameter-table.md)), these features are removed from the dataframe that is returned from FreqAI to the strategy. To keep a particular type of feature for plotting purposes, you would prepend it with `%%`.
**Datatype:** Depends on the output of the model. ## Setting the `startup_candle_count` -The `startup_candle_count` in the `FreqAI` strategy needs to be set up in the same way as in the standard 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 the `dataprovider`, to avoid any NaNs at the beginning of the first training. You can easily set this value by identifying the longest period (in candle units) which is passed to the indicator creation functions (e.g., Ta-Lib functions). In the presented example, `startup_candle_count` is 20 since this is the maximum value in `indicators_periods_candles`. +The `startup_candle_count` in the FreqAI strategy needs to be set up in the same way as in the standard 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 the `dataprovider`, to avoid any NaNs at the beginning of the first training. You can easily set this value by identifying the longest period (in candle units) which is passed to the indicator creation functions (e.g., Ta-Lib functions). In the presented example, `startup_candle_count` is 20 since this is the maximum value in `indicators_periods_candles`. !!! Note There are instances where the Ta-Lib functions actually require more data than just the passed `period` or else the feature dataset gets populated with NaNs. Anecdotally, multiplying the `startup_candle_count` by 2 always leads to a fully NaN free training dataset. Hence, it is typically safest to multiply the expected `startup_candle_count` by 2. Look out for this log message to confirm that the data is clean: @@ -185,7 +185,7 @@ The `startup_candle_count` in the `FreqAI` strategy needs to be set up in the sa ## Creating a dynamic target threshold -Deciding when to enter or exit a trade can be done in a dynamic way to reflect current market conditions. `FreqAI` allows you to return additional information from the training of a model (more info [here](freqai-feature-engineering.md#returning-additional-info-from-training)). For example, the `&*_std/mean` return values describe the statistical distribution of the target/label *during the most recent training*. Comparing a given prediction to these values allows you to know the rarity of the prediction. In `templates/FreqaiExampleStrategy.py`, the `target_roi` and `sell_roi` are defined to be 1.25 z-scores away from the mean which causes predictions that are closer to the mean to be filtered out. +Deciding when to enter or exit a trade can be done in a dynamic way to reflect current market conditions. FreqAI allows you to return additional information from the training of a model (more info [here](freqai-feature-engineering.md#returning-additional-info-from-training)). For example, the `&*_std/mean` return values describe the statistical distribution of the target/label *during the most recent training*. Comparing a given prediction to these values allows you to know the rarity of the prediction. In `templates/FreqaiExampleStrategy.py`, the `target_roi` and `sell_roi` are defined to be 1.25 z-scores away from the mean which causes predictions that are closer to the mean to be filtered out. ```python dataframe["target_roi"] = dataframe["&-s_close_mean"] + dataframe["&-s_close_std"] * 1.25 @@ -200,15 +200,15 @@ To consider the population of *historical predictions* for creating the dynamic } ``` -If this value is set, `FreqAI` will initially use the predictions from the training data and subsequently begin introducing real prediction data as it is generated. `FreqAI` will save this historical data to be reloaded if you stop and restart a model with the same `identifier`. +If this value is set, FreqAI will initially use the predictions from the training data and subsequently begin introducing real prediction data as it is generated. FreqAI will save this historical data to be reloaded if you stop and restart a model with the same `identifier`. ## Using different prediction models -`FreqAI` has multiple example prediction model libraries that are ready to be used as is via the flag `--freqaimodel`. These libraries include `Catboost`, `LightGBM`, and `XGBoost` regression, classification, and multi-target models, and can be found in `freqai/prediction_models/`. However, it is possible to customize and create your own prediction models using the `IFreqaiModel` class. You are encouraged to inherit `fit()`, `train()`, and `predict()` to let these customize various aspects of the training procedures. +FreqAI has multiple example prediction model libraries that are ready to be used as is via the flag `--freqaimodel`. These libraries include `Catboost`, `LightGBM`, and `XGBoost` regression, classification, and multi-target models, and can be found in `freqai/prediction_models/`. However, it is possible to customize and create your own prediction models using the `IFreqaiModel` class. You are encouraged to inherit `fit()`, `train()`, and `predict()` to let these customize various aspects of the training procedures. ### Setting classifier targets -`FreqAI` includes a variety of classifiers, such as the `CatboostClassifier` via the flag `--freqaimodel CatboostClassifier`. If you elects to use a classifier, the classes need to be set using strings. For example: +FreqAI includes a variety of classifiers, such as the `CatboostClassifier` via the flag `--freqaimodel CatboostClassifier`. If you elects to use a classifier, the classes need to be set using strings. For example: ```python df['&s-up_or_down'] = np.where( df["close"].shift(-100) > df["close"], 'up', 'down') diff --git a/docs/freqai-developers.md b/docs/freqai-developers.md index 9794e0efa..37f4543b7 100644 --- a/docs/freqai-developers.md +++ b/docs/freqai-developers.md @@ -2,13 +2,13 @@ ## Project architecture -The architecture and functions of `FreqAI` are generalized to encourages development of unique features, functions, models, etc. +The architecture and functions of FreqAI are generalized to encourages development of unique features, functions, models, etc. The class structure and a detailed algorithmic overview is depicted in the following diagram: ![image](assets/freqai_algorithm-diagram.jpg) -As shown, there are three distinct objects comprising `FreqAI`: +As shown, there are three distinct objects comprising FreqAI: * **IFreqaiModel** - A singular persistent object containing all the necessary logic to collect, store, and process data, engineer features, run training, and inference models. * **FreqaiDataKitchen** - A non-persistent object which is created uniquely for each unique asset/model. Beyond metadata, it also contains a variety of data processing tools. @@ -18,7 +18,7 @@ There are a variety of built-in [prediction models](freqai-configuration.md#usin ## Data handling -`FreqAI` aims to organize model files, prediction data, and meta data in a way that simplifies post-processing and enhances crash resilience by automatic data reloading. The data is saved in a file structure,`user_data_dir/models/`, which contains all the data associated with the trainings and backtests. The `FreqaiDataKitchen()` relies heavily on the file structure for proper training and inferencing and should therefore not be manually modified. +FreqAI aims to organize model files, prediction data, and meta data in a way that simplifies post-processing and enhances crash resilience by automatic data reloading. The data is saved in a file structure,`user_data_dir/models/`, which contains all the data associated with the trainings and backtests. The `FreqaiDataKitchen()` relies heavily on the file structure for proper training and inferencing and should therefore not be manually modified. ### File structure @@ -27,7 +27,7 @@ The file structure is automatically generated based on the model `identifier` se | Structure | Description | |-----------|-------------| | `config_*.json` | A copy of the model specific configuration file. | -| `historic_predictions.pkl` | A file containing all historic predictions generated during the lifetime of the `identifier` model during live deployment. `historic_predictions.pkl` is used to reload the model after a crash or a config change. A backup file is always held in case of corruption on the main file.`FreqAI`**automatically** detects corruption and replaces the corrupted file with the backup. | +| `historic_predictions.pkl` | A file containing all historic predictions generated during the lifetime of the `identifier` model during live deployment. `historic_predictions.pkl` is used to reload the model after a crash or a config change. A backup file is always held in case of corruption on the main file. FreqAI **automatically** detects corruption and replaces the corrupted file with the backup. | | `pair_dictionary.json` | A file containing the training queue as well as the on disk location of the most recently trained model. | | `sub-train-*_TIMESTAMP` | A folder containing all the files associated with a single model, such as:
|| `*_metadata.json` - Metadata for the model, such as normalization max/min, expected training feature list, etc.
diff --git a/docs/freqai-feature-engineering.md b/docs/freqai-feature-engineering.md index bd700bbf7..b7c23aa60 100644 --- a/docs/freqai-feature-engineering.md +++ b/docs/freqai-feature-engineering.md @@ -4,7 +4,7 @@ Low level feature engineering is performed in the user strategy within a function called `populate_any_indicators()`. That function sets the `base features` such as, `RSI`, `MFI`, `EMA`, `SMA`, time of day, volume, etc. The `base features` can be custom indicators or they can be imported from any technical-analysis library that you can find. One important syntax rule is that all `base features` string names are prepended with `%`, while labels/targets are prepended with `&`. -Meanwhile, high level feature engineering is handled within `"feature_parameters":{}` in the `FreqAI` config. Within this file, it is possible to decide large scale feature expansions on top of the `base_features` such as "including correlated pairs" or "including informative timeframes" or even "including recent candles." +Meanwhile, high level feature engineering is handled within `"feature_parameters":{}` in the FreqAI config. Within this file, it is possible to decide large scale feature expansions on top of the `base_features` such as "including correlated pairs" or "including informative timeframes" or even "including recent candles." It is advisable to start from the template `populate_any_indicators()` in the source provided example strategy (found in `templates/FreqaiExampleStrategy.py`) to ensure that the feature definitions are following the correct conventions. Here is an example of how to set the indicators and labels in the strategy: @@ -122,7 +122,7 @@ The `include_timeframes` in the config above are the timeframes (`tf`) of each c You can ask for each of the defined features to be included also for informative pairs using the `include_corr_pairlist`. This means that the feature set will include all the features from `populate_any_indicators` on all the `include_timeframes` for each of the correlated pairs defined in the config (`ETH/USD`, `LINK/USD`, and `BNB/USD` in the presented example). -`include_shifted_candles` indicates the number of previous candles to include in the feature set. For example, `include_shifted_candles: 2` tells `FreqAI` to include the past 2 candles for each of the features in the feature set. +`include_shifted_candles` indicates the number of previous candles to include in the feature set. For example, `include_shifted_candles: 2` tells FreqAI to include the past 2 candles for each of the features in the feature set. In total, the number of features the user of the presented example strat has created is: length of `include_timeframes` * no. features in `populate_any_indicators()` * length of `include_corr_pairlist` * no. `include_shifted_candles` * length of `indicator_periods_candles` $= 3 * 3 * 3 * 2 * 2 = 108$. @@ -131,7 +131,7 @@ In total, the number of features the user of the presented example strat has cre Important metrics can be returned to the strategy at the end of each model training by assigning them to `dk.data['extra_returns_per_train']['my_new_value'] = XYZ` inside the custom prediction model class. -`FreqAI` takes the `my_new_value` assigned in this dictionary and expands it to fit the dataframe that is returned to the strategy. You can then use the returned metrics in your strategy through `dataframe['my_new_value']`. An example of how return values can be used in `FreqAI` are the `&*_mean` and `&*_std` values that are used to [created a dynamic target threshold](freqai-configuration.md#creating-a-dynamic-target-threshold). +FreqAI takes the `my_new_value` assigned in this dictionary and expands it to fit the dataframe that is returned to the strategy. You can then use the returned metrics in your strategy through `dataframe['my_new_value']`. An example of how return values can be used in FreqAI are the `&*_mean` and `&*_std` values that are used to [created a dynamic target threshold](freqai-configuration.md#creating-a-dynamic-target-threshold). Another example, where the user wants to use live metrics from the trade database, is shown below: @@ -141,15 +141,15 @@ Another example, where the user wants to use live metrics from the trade databas } ``` -You need to set the standard dictionary in the config so that `FreqAI` can return proper dataframe shapes. These values will likely be overridden by the prediction model, but in the case where the model has yet to set them, or needs a default initial value, the pre-set values are what will be returned. +You need to set the standard dictionary in the config so that FreqAI can return proper dataframe shapes. These values will likely be overridden by the prediction model, but in the case where the model has yet to set them, or needs a default initial value, the pre-set values are what will be returned. ## Feature normalization -`FreqAI` is strict when it comes to data normalization. The train features, $X^{train}$, are always normalized to [-1, 1] using a shifted min-max normalization: +FreqAI is strict when it comes to data normalization. The train features, $X^{train}$, are always normalized to [-1, 1] using a shifted min-max normalization: $$X^{train}_{norm} = 2 * \frac{X^{train} - X^{train}.min()}{X^{train}.max() - X^{train}.min()} - 1$$ -All other data (test data and unseen prediction data in dry/live/backtest) is always automatically normalized to the training feature space according to industry standards. `FreqAI` stores all the metadata required to ensure that test and prediction features will be properly normalized and that predictions are properly denormalized. For this reason, it is not recommended to eschew industry standards and modify `FreqAI` internals - however - advanced users can do so by inheriting `train()` in their custom `IFreqaiModel` and using their own normalization functions. +All other data (test data and unseen prediction data in dry/live/backtest) is always automatically normalized to the training feature space according to industry standards. FreqAI stores all the metadata required to ensure that test and prediction features will be properly normalized and that predictions are properly denormalized. For this reason, it is not recommended to eschew industry standards and modify FreqAI internals - however - advanced users can do so by inheriting `train()` in their custom `IFreqaiModel` and using their own normalization functions. ## Data dimensionality reduction with Principal Component Analysis @@ -169,17 +169,17 @@ This will perform PCA on the features and reduce their dimensionality so that th The `inlier_metric` is a metric aimed at quantifying how similar a the features of a data point are to the most recent historic data points. -You define the lookback window by setting `inlier_metric_window` and `FreqAI` computes the distance between the present time point and each of the previous `inlier_metric_window` lookback points. A Weibull function is fit to each of the lookback distributions and its cumulative distribution function (CDF) is used to produce a quantile for each lookback point. The `inlier_metric` is then computed for each time point as the average of the corresponding lookback quantiles. The figure below explains the concept for an `inlier_metric_window` of 5. +You define the lookback window by setting `inlier_metric_window` and FreqAI computes the distance between the present time point and each of the previous `inlier_metric_window` lookback points. A Weibull function is fit to each of the lookback distributions and its cumulative distribution function (CDF) is used to produce a quantile for each lookback point. The `inlier_metric` is then computed for each time point as the average of the corresponding lookback quantiles. The figure below explains the concept for an `inlier_metric_window` of 5. ![inlier-metric](assets/freqai_inlier-metric.jpg) -`FreqAI` adds the `inlier_metric` to the training features and hence gives the model access to a novel type of temporal information. +FreqAI adds the `inlier_metric` to the training features and hence gives the model access to a novel type of temporal information. This function does **not** remove outliers from the data set. ## Weighting features for temporal importance -`FreqAI` allows you to set a `weight_factor` to weight recent data more strongly than past data via an exponential function: +FreqAI allows you to set a `weight_factor` to weight recent data more strongly than past data via an exponential function: $$ W_i = \exp(\frac{-i}{\alpha*n}) $$ @@ -189,13 +189,13 @@ where $W_i$ is the weight of data point $i$ in a total set of $n$ data points. B ## Outlier detection -Equity and crypto markets suffer from a high level of non-patterned noise in the form of outlier data points. `FreqAI` implements a variety of methods to identify such outliers and hence mitigate risk. +Equity and crypto markets suffer from a high level of non-patterned noise in the form of outlier data points. FreqAI implements a variety of methods to identify such outliers and hence mitigate risk. ### Identifying outliers with the Dissimilarity Index (DI) The Dissimilarity Index (DI) aims to quantify the uncertainty associated with each prediction made by the model. -You can tell `FreqAI` to remove outlier data points from the training/test data sets using the DI by including the following statement in the config: +You can tell FreqAI to remove outlier data points from the training/test data sets using the DI by including the following statement in the config: ```json "freqai": { @@ -205,7 +205,7 @@ You can tell `FreqAI` to remove outlier data points from the training/test data } ``` - The DI allows predictions which are outliers (not existent in the model feature space) to be thrown out due to low levels of certainty. To do so, `FreqAI` measures the distance between each training data point (feature vector), $X_{a}$, and all other training data points: + The DI allows predictions which are outliers (not existent in the model feature space) to be thrown out due to low levels of certainty. To do so, FreqAI measures the distance between each training data point (feature vector), $X_{a}$, and all other training data points: $$ d_{ab} = \sqrt{\sum_{j=1}^p(X_{a,j}-X_{b,j})^2} $$ @@ -229,7 +229,7 @@ Below is a figure that describes the DI for a 3D data set. ### Identifying outliers using a Support Vector Machine (SVM) -You can tell `FreqAI` to remove outlier data points from the training/test data sets using a Support Vector Machine (SVM) by including the following statement in the config: +You can tell FreqAI to remove outlier data points from the training/test data sets using a Support Vector Machine (SVM) by including the following statement in the config: ```json "freqai": { @@ -241,7 +241,7 @@ You can tell `FreqAI` to remove outlier data points from the training/test data The SVM will be trained on the training data and any data point that the SVM deems to be beyond the feature space will be removed. -`FreqAI` uses `sklearn.linear_model.SGDOneClassSVM` (details are available on scikit-learn's webpage [here](https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.SGDOneClassSVM.html) (external website)) and you can elect to provide additional parameters for the SVM, such as `shuffle`, and `nu`. +FreqAI uses `sklearn.linear_model.SGDOneClassSVM` (details are available on scikit-learn's webpage [here](https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.SGDOneClassSVM.html) (external website)) and you can elect to provide additional parameters for the SVM, such as `shuffle`, and `nu`. The parameter `shuffle` is by default set to `False` to ensure consistent results. If it is set to `True`, running the SVM multiple times on the same data set might result in different outcomes due to `max_iter` being to low for the algorithm to reach the demanded `tol`. Increasing `max_iter` solves this issue but causes the procedure to take longer time. @@ -249,7 +249,7 @@ The parameter `nu`, *very* broadly, is the amount of data points that should be ### Identifying outliers with DBSCAN -You can configure `FreqAI` to use DBSCAN to cluster and remove outliers from the training/test data set or incoming outliers from predictions, by activating `use_DBSCAN_to_remove_outliers` in the config: +You can configure FreqAI to use DBSCAN to cluster and remove outliers from the training/test data set or incoming outliers from predictions, by activating `use_DBSCAN_to_remove_outliers` in the config: ```json "freqai": { @@ -265,4 +265,4 @@ 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) (external website)) with `min_samples` ($N$) taken as 1/4 of the no. of time points (candles) in the feature set. `eps` ($\varepsilon$) is computed automatically 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. +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) (external website)) with `min_samples` ($N$) taken as 1/4 of the no. of time points (candles) in the feature set. `eps` ($\varepsilon$) is computed automatically 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. diff --git a/docs/freqai-parameter-table.md b/docs/freqai-parameter-table.md index 2ec8fac30..38d7ece94 100644 --- a/docs/freqai-parameter-table.md +++ b/docs/freqai-parameter-table.md @@ -1,13 +1,13 @@ # Parameter table -The table below will list all configuration parameters available for `FreqAI`. Some of the parameters are exemplified in `config_examples/config_freqai.example.json`. +The table below will list all configuration parameters available for FreqAI. Some of the parameters are exemplified in `config_examples/config_freqai.example.json`. Mandatory parameters are marked as **Required** and have to be set in one of the suggested ways. | Parameter | Description | |------------|-------------| | | **General configuration parameters** -| `freqai` | **Required.**
The parent dictionary containing all the parameters for controlling `FreqAI`.
**Datatype:** Dictionary. +| `freqai` | **Required.**
The parent dictionary containing all the parameters for controlling FreqAI.
**Datatype:** Dictionary. | `train_period_days` | **Required.**
Number of days to use for the training data (width of the sliding window).
**Datatype:** Positive integer. | `backtest_period_days` | **Required.**
Number of days to inference from the trained model before sliding the `train_period_days` window defined above, and retraining the model during backtesting (more info [here](freqai-running.md#backtesting)). This can be fractional days, but beware that the provided `timerange` will be divided by this number to yield the number of trainings necessary to complete the backtest.
**Datatype:** Float. | `identifier` | **Required.**
A unique ID for the current model. If models are saved to disk, the `identifier` allows for reloading specific pre-trained models/data.
**Datatype:** String. @@ -21,11 +21,11 @@ Mandatory parameters are marked as **Required** and have to be set in one of the | | **Feature parameters** | `feature_parameters` | A dictionary containing the parameters used to engineer the feature set. Details and examples are shown [here](freqai-feature-engineering.md).
**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 indicators dataset.
**Datatype:** List of timeframes (strings). -| `include_corr_pairlist` | A list of correlated coins that `FreqAI` will add as additional features to all `pair_whitelist` coins. All indicators set in `populate_any_indicators` during feature engineering (see details [here](freqai-feature-engineering.md)) will be created for each correlated coin. The correlated coins features are added to the base indicators dataset.
**Datatype:** List of assets (strings). +| `include_corr_pairlist` | A list of correlated coins that FreqAI will add as additional features to all `pair_whitelist` coins. All indicators set in `populate_any_indicators` during feature engineering (see details [here](freqai-feature-engineering.md)) will be created for each correlated coin. The correlated coins features are added to the base indicators dataset.
**Datatype:** List of assets (strings). | `label_period_candles` | Number of candles into the future that the labels are created for. This is used in `populate_any_indicators` (see `templates/FreqaiExampleStrategy.py` for detailed usage). You can create custom labels and choose whether to make use of this parameter or not.
**Datatype:** Positive integer. -| `include_shifted_candles` | Add features from previous candles to subsequent candles with the intent of adding historical information. If used, `FreqAI` will duplicate and shift all features from the `include_shifted_candles` previous candles so that the information is available for the subsequent candle.
**Datatype:** Positive integer. +| `include_shifted_candles` | Add features from previous candles to subsequent candles with the intent of adding historical information. If used, FreqAI will duplicate and shift all features from the `include_shifted_candles` previous candles so that the information is available for the subsequent candle.
**Datatype:** Positive integer. | `weight_factor` | Weight training data points according to their recency (see details [here](freqai-feature-engineering.md#weighting-features-for-temporal-importance)).
**Datatype:** Positive float (typically < 1). -| `indicator_max_period_candles` | **No longer used (#7325)**. Replaced by `startup_candle_count` which is set in the [strategy](freqai-configuration.md#building-a-freqai-strategy). `startup_candle_count` is timeframe independent and defines the maximum *period* used in `populate_any_indicators()` for indicator creation. `FreqAI` uses this parameter together with the maximum timeframe in `include_time_frames` to calculate how many data points to download such that the first data point does not include a NaN.
**Datatype:** Positive integer. +| `indicator_max_period_candles` | **No longer used (#7325)**. Replaced by `startup_candle_count` which is set in the [strategy](freqai-configuration.md#building-a-freqai-strategy). `startup_candle_count` is timeframe independent and defines the maximum *period* used in `populate_any_indicators()` for indicator creation. FreqAI uses this parameter together with the maximum timeframe in `include_time_frames` to calculate how many data points to download such that the first data point does not include a NaN.
**Datatype:** Positive integer. | `indicator_periods_candles` | Time periods to calculate indicators for. The indicators are added to the base indicator dataset.
**Datatype:** List of positive integers. | `principal_component_analysis` | Automatically reduce the dimensionality of the data set using Principal Component Analysis. See details about how it works [here](#reducing-data-dimensionality-with-principal-component-analysis)
**Datatype:** Boolean.
Default: `False`. | `plot_feature_importances` | Create a feature importance plot for each model for the top/bottom `plot_feature_importances` number of features.
**Datatype:** Integer.
Default: `0`. @@ -33,9 +33,9 @@ Mandatory parameters are marked as **Required** and have to be set in one of the | `use_SVM_to_remove_outliers` | Train a support vector machine to detect and remove outliers from the training dataset, as well as from incoming data points. See details about how it works [here](freqai-feature-engineering.md#identifying-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](freqai-feature-engineering.md#identifying-outliers-using-a-support-vector-machine-svm).
**Datatype:** Dictionary. | `use_DBSCAN_to_remove_outliers` | Cluster data using the DBSCAN algorithm to identify and remove outliers from training and prediction data. See details about how it works [here](freqai-feature-engineering.md#identifying-outliers-with-dbscan).
**Datatype:** Boolean. -| `inlier_metric_window` | If set, `FreqAI` adds an `inlier_metric` to the training feature set and set the lookback to be the `inlier_metric_window`, i.e., the number of previous time points to compare the current candle to. Details of how the `inlier_metric` is computed can be found [here](freqai-feature-engineering.md#inlier-metric).
**Datatype:** Integer.
Default: `0`. -| `noise_standard_deviation` | If set, `FreqAI` adds noise to the training features with the aim of preventing overfitting. `FreqAI` generates random deviates from a gaussian distribution with a standard deviation of `noise_standard_deviation` and adds them to all data points. `noise_standard_deviation` should be kept relative to the normalized space, i.e., between -1 and 1. In other words, since data in `FreqAI` is always normalized to be between -1 and 1, `noise_standard_deviation: 0.05` would result in 32% of the data being randomly increased/decreased by more than 2.5% (i.e., the percent of data falling within the first standard deviation).
**Datatype:** Integer.
Default: `0`. -| `outlier_protection_percentage` | Enable to prevent outlier detection methods from discarding too much data. 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, i.e., the original dataset will be kept intact. If the outlier protection is triggered, no predictions will be made based on the training dataset.
**Datatype:** Float.
Default: `30`. +| `inlier_metric_window` | If set, FreqAI adds an `inlier_metric` to the training feature set and set the lookback to be the `inlier_metric_window`, i.e., the number of previous time points to compare the current candle to. Details of how the `inlier_metric` is computed can be found [here](freqai-feature-engineering.md#inlier-metric).
**Datatype:** Integer.
Default: `0`. +| `noise_standard_deviation` | If set, FreqAI adds noise to the training features with the aim of preventing overfitting. FreqAI generates random deviates from a gaussian distribution with a standard deviation of `noise_standard_deviation` and adds them to all data points. `noise_standard_deviation` should be kept relative to the normalized space, i.e., between -1 and 1. In other words, since data in FreqAI is always normalized to be between -1 and 1, `noise_standard_deviation: 0.05` would result in 32% of the data being randomly increased/decreased by more than 2.5% (i.e., the percent of data falling within the first standard deviation).
**Datatype:** Integer.
Default: `0`. +| `outlier_protection_percentage` | Enable to prevent outlier detection methods from discarding too much data. 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, i.e., the original dataset will be kept intact. If the outlier protection is triggered, no predictions will be made based on the training dataset.
**Datatype:** Float.
Default: `30`. | `reverse_train_test_order` | Split the feature dataset (see below) and use the latest data split for training 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, you should be careful to understand the unorthodox nature of this parameter before employing it.
**Datatype:** Boolean.
Default: `False` (no reversal). | | **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. diff --git a/docs/freqai-running.md b/docs/freqai-running.md index 6299a80f3..b8994aed9 100644 --- a/docs/freqai-running.md +++ b/docs/freqai-running.md @@ -1,18 +1,18 @@ -# Running `FreqAI` +# Running FreqAI -There are two ways to train and deploy an adaptive machine learning model - live deployment and historical backtesting. In both cases, `FreqAI` runs/simulates periodic retraining of models as shown in the following figure: +There are two ways to train and deploy an adaptive machine learning model - live deployment and historical backtesting. In both cases, FreqAI runs/simulates periodic retraining of models as shown in the following figure: ![freqai-window](assets/freqai_moving-window.jpg) ## Live deployments -`FreqAI` can be run dry/live using the following command: +FreqAI can be run dry/live using the following command: ```bash freqtrade trade --strategy FreqaiExampleStrategy --config config_freqai.example.json --freqaimodel LightGBMRegressor ``` -When launched, `FreqAI` will start training a new model, with a new `identifier`, based on the config settings. Following training, the model will be used to make predictions on incoming candles until a new model is available. New models are typically generated as often as possible, with `FreqAI` managing an internal queue of the coin pairs to try to keep all models equally up to date. `FreqAI` will always use the most recently trained model to make predictions on incoming live data. If you do not want `FreqAI` to retrain new models as often as possible, you can set `live_retrain_hours` to tell `FreqAI` to wait at least that number of hours before training a new model. Additionally, you can set `expired_hours` to tell `FreqAI` to avoid making predictions on models that are older than that number of hours. +When launched, FreqAI will start training a new model, with a new `identifier`, based on the config settings. Following training, the model will be used to make predictions on incoming candles until a new model is available. New models are typically generated as often as possible, with FreqAI managing an internal queue of the coin pairs to try to keep all models equally up to date. FreqAI will always use the most recently trained model to make predictions on incoming live data. If you do not want FreqAI to retrain new models as often as possible, you can set `live_retrain_hours` to tell FreqAI to wait at least that number of hours before training a new model. Additionally, you can set `expired_hours` to tell FreqAI to avoid making predictions on models that are older than that number of hours. Trained models are by default saved to disk to allow for reuse during backtesting or after a crash. You can opt to [purge old models](#purging-old-model-data) to save disk space by setting `"purge_old_models": true` in the config. @@ -25,11 +25,11 @@ To start a dry/live run from a saved backtest model (or from a previously crashe } ``` -In this case, although `FreqAI` will initiate with a pre-trained model, it will still check to see how much time has elapsed since the model was trained. If a full `live_retrain_hours` has elapsed since the end of the loaded model, `FreqAI` will start training a new model. +In this case, although FreqAI will initiate with a pre-trained model, it will still check to see how much time has elapsed since the model was trained. If a full `live_retrain_hours` has elapsed since the end of the loaded model, FreqAI will start training a new model. ### Automatic data download -`FreqAI` automatically downloads the proper amount of data needed to ensure training of a model through the defined `train_period_days` and `startup_candle_count` (see the [parameter table](freqai-parameter-table.md) for detailed descriptions of these parameters). +FreqAI automatically downloads the proper amount of data needed to ensure training of a model through the defined `train_period_days` and `startup_candle_count` (see the [parameter table](freqai-parameter-table.md) for detailed descriptions of these parameters). ### Saving prediction data @@ -37,7 +37,7 @@ All predictions made during the lifetime of a specific `identifier` model are st ### Purging old model data -`FreqAI` stores new model files after each successful training. These files become obsolete as new models are generated to adapt to new market conditions. If you are planning to leave `FreqAI` running for extended periods of time with high frequency retraining, you should enable `purge_old_models` in the config: +FreqAI stores new model files after each successful training. These files become obsolete as new models are generated to adapt to new market conditions. If you are planning to leave FreqAI running for extended periods of time with high frequency retraining, you should enable `purge_old_models` in the config: ```json "freqai": { @@ -49,20 +49,20 @@ This will automatically purge all models older than the two most recently traine ## Backtesting -The `FreqAI` backtesting module can be executed with the following command: +The FreqAI backtesting module can be executed with the following command: ```bash freqtrade backtesting --strategy FreqaiExampleStrategy --strategy-path freqtrade/templates --config config_examples/config_freqai.example.json --freqaimodel LightGBMRegressor --timerange 20210501-20210701 ``` -If this command has never been executed with the existing config file, `FreqAI` will train a new model +If this command has never been executed with the existing config file, FreqAI will train a new model for each pair, for each backtesting window within the expanded `--timerange`. -Backtesting mode requires [downloading the necessary data](#downloading-data-to-cover-the-full-backtest-period) before deployment (unlike in dry/live mode where `FreqAI` handles the data downloading automatically). You 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 set backtesting time range. More details on how to calculate the data to download can be found [here](#deciding-the-size-of-the-sliding-training-window-and-backtesting-duration). +Backtesting mode requires [downloading the necessary data](#downloading-data-to-cover-the-full-backtest-period) before deployment (unlike in dry/live mode where FreqAI handles the data downloading automatically). You 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 set backtesting time range. More details on how to calculate the data to download can be found [here](#deciding-the-size-of-the-sliding-training-window-and-backtesting-duration). !!! Note "Model reuse" Once the training is completed, you can execute the backtesting again with the same config file and - `FreqAI` will find the trained models and load them instead of spending time training. This is useful + FreqAI will find the trained models and load them instead of spending time training. This is useful if you want to tweak (or even hyperopt) buy and sell criteria inside the strategy. If you *want* to retrain a new model with the same config file, you should simply change the `identifier`. This way, you can return to using any model you wish by simply specifying the `identifier`. @@ -71,31 +71,31 @@ Backtesting mode requires [downloading the necessary data](#downloading-data-to- ### Saving prediction data -To allow for tweaking your strategy (**not** the features!), `FreqAI` will automatically save the predictions during backtesting so that they can be reused for future backtests and live runs using the same `identifier` model. This provides a performance enhancement geared towards enabling **high-level hyperopting** of entry/exit criteria. +To allow for tweaking your strategy (**not** the features!), FreqAI will automatically save the predictions during backtesting so that they can be reused for future backtests and live runs using the same `identifier` model. This provides a performance enhancement geared towards enabling **high-level hyperopting** of entry/exit criteria. An additional directory called `predictions`, which contains all the predictions stored in `hdf` format, will be created in the `unique-id` folder. -To change your **features**, you **must** set a new `identifier` in the config to signal to `FreqAI` to train new models. +To change your **features**, you **must** set a new `identifier` in the config to signal to FreqAI to train new models. To save the models generated during a particular backtest so that you can start a live deployment from one of them instead of training a new model, you must set `save_backtest_models` to `True` in the config. ### Downloading data to cover the full backtest period -For live/dry deployments, `FreqAI` will download the necessary data automatically. However, to use backtesting functionality, you need to download the necessary data using `download-data` (details [here](data-download.md#data-downloading)). You need to pay careful attention to understanding how much *additional* data needs to be downloaded to ensure that there is a sufficient amount of training data *before* the start of the backtesting time range. The amount of additional data can be roughly estimated by moving the start date of the time range backwards by `train_period_days` and the `startup_candle_count` (see the [parameter table](freqai-parameter-table.md) for detailed descriptions of these parameters) from the beginning of the desired backtesting time range. +For live/dry deployments, FreqAI will download the necessary data automatically. However, to use backtesting functionality, you need to download the necessary data using `download-data` (details [here](data-download.md#data-downloading)). You need to pay careful attention to understanding how much *additional* data needs to be downloaded to ensure that there is a sufficient amount of training data *before* the start of the backtesting time range. The amount of additional data can be roughly estimated by moving the start date of the time range backwards by `train_period_days` and the `startup_candle_count` (see the [parameter table](freqai-parameter-table.md) for detailed descriptions of these parameters) from the beginning of the desired backtesting time range. As an example, to backtest the `--timerange 20210501-20210701` using the [example config](freqai-configuration.md#setting-up-the-configuration-file) which sets `train_period_days` to 30, together with `startup_candle_count: 40` on a maximum `include_timeframes` of 1h, the start date for the downloaded data needs to be `20210501` - 30 days - 40 * 1h / 24 hours = 20210330 (31.7 days earlier than the start of the desired training time range). ### Deciding the size of the sliding training window and backtesting duration The backtesting time range is defined with the typical `--timerange` parameter in the configuration file. The duration of the sliding training window is set by `train_period_days`, whilst `backtest_period_days` is the sliding backtesting window, both in number of days (`backtest_period_days` can be -a float to indicate sub-daily retraining in live/dry mode). In the presented [example config](freqai-configuration.md#setting-up-the-configuration-file) (found in `config_examples/config_freqai.example.json`), the user is asking `FreqAI` to use a training period of 30 days and backtest on the subsequent 7 days. After the training of the model, `FreqAI` will backtest the subsequent 7 days. The "sliding window" then moves one week forward (emulating `FreqAI` retraining once per week in live mode) and the new model uses the previous 30 days (including the 7 days used for backtesting by the previous model) to train. This is repeated until the end of `--timerange`. This means that if you set `--timerange 20210501-20210701`, `FreqAI` will have trained 8 separate models at the end of `--timerange` (because the full range comprises 8 weeks). +a float to indicate sub-daily retraining in live/dry mode). In the presented [example config](freqai-configuration.md#setting-up-the-configuration-file) (found in `config_examples/config_freqai.example.json`), the user is asking FreqAI to use a training period of 30 days and backtest on the subsequent 7 days. After the training of the model, FreqAI will backtest the subsequent 7 days. The "sliding window" then moves one week forward (emulating FreqAI retraining once per week in live mode) and the new model uses the previous 30 days (including the 7 days used for backtesting by the previous model) to train. This is repeated until the end of `--timerange`. This means that if you set `--timerange 20210501-20210701`, FreqAI will have trained 8 separate models at the end of `--timerange` (because the full range comprises 8 weeks). !!! Note - Although fractional `backtest_period_days` is allowed, you 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, by setting a `--timerange` of 10 days, and 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 train constantly. In this case, backtesting would take the exact same amount of time as a dry run. + Although fractional `backtest_period_days` is allowed, you 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, by setting a `--timerange` of 10 days, and 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 train constantly. In this case, backtesting would take the exact same amount of time as a dry run. ## Defining model expirations -During dry/live mode, `FreqAI` trains each coin pair sequentially (on separate threads/GPU from the main Freqtrade bot). This means that there is always an age discrepancy between models. If you are training on 50 pairs, and each pair requires 5 minutes to train, the oldest model will be over 4 hours old. This may be undesirable if the characteristic time scale (the trade duration target) for a strategy is less than 4 hours. You can decide to only make trade entries if the model is less than a certain number of hours old by setting the `expiration_hours` in the config file: +During dry/live mode, FreqAI trains each coin pair sequentially (on separate threads/GPU from the main Freqtrade bot). This means that there is always an age discrepancy between models. If you are training on 50 pairs, and each pair requires 5 minutes to train, the oldest model will be over 4 hours old. This may be undesirable if the characteristic time scale (the trade duration target) for a strategy is less than 4 hours. You can decide to only make trade entries if the model is less than a certain number of hours old by setting the `expiration_hours` in the config file: ```json "freqai": { @@ -107,11 +107,11 @@ In the presented example config, the user will only allow predictions on models ## Controlling the model learning process -Model training parameters are unique to the selected machine learning library. `FreqAI` allows you to set any parameter for any library using the `model_training_parameters` dictionary in the config. The example config (found in `config_examples/config_freqai.example.json`) shows some of the example parameters associated with `Catboost` and `LightGBM`, but you can add any parameters available in those libraries or any other machine learning library you choose to implement. +Model training parameters are unique to the selected machine learning library. FreqAI allows you to set any parameter for any library using the `model_training_parameters` dictionary in the config. The example config (found in `config_examples/config_freqai.example.json`) shows some of the example parameters associated with `Catboost` and `LightGBM`, but you can add any parameters available in those libraries or any other machine learning library you choose to implement. Data split parameters are defined in `data_split_parameters` which can be any parameters associated with Scikit-learn's `train_test_split()` function. `train_test_split()` has a parameters called `shuffle` which allows to shuffle the data or keep it unshuffled. This is particularly useful to avoid biasing training with temporally auto-correlated data. More details about these parameters can be found the [Scikit-learn website](https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.train_test_split.html) (external website). -The `FreqAI` specific parameter `label_period_candles` defines the offset (number of candles into the future) used for the `labels`. In the presented [example config](freqai-configuration.md#setting-up-the-configuration-file), the user is asking for `labels` that are 24 candles in the future. +The FreqAI specific parameter `label_period_candles` defines the offset (number of candles into the future) used for the `labels`. In the presented [example config](freqai-configuration.md#setting-up-the-configuration-file), the user is asking for `labels` that are 24 candles in the future. ## Continual learning @@ -125,15 +125,15 @@ You can hyperopt using the same command as for [typical Freqtrade hyperopt](hype freqtrade hyperopt --hyperopt-loss SharpeHyperOptLoss --strategy FreqaiExampleStrategy --freqaimodel LightGBMRegressor --strategy-path freqtrade/templates --config config_examples/config_freqai.example.json --timerange 20220428-20220507 ``` -`hyperopt` requires you to have the data pre-downloaded in the same fashion as if you were doing [backtesting](#backtesting). In addition, you must consider some restrictions when trying to hyperopt `FreqAI` strategies: +`hyperopt` requires you to have the data pre-downloaded in the same fashion as if you were doing [backtesting](#backtesting). In addition, you must consider some restrictions when trying to hyperopt FreqAI strategies: -- The `--analyze-per-epoch` hyperopt parameter is not compatible with `FreqAI`. +- The `--analyze-per-epoch` hyperopt parameter is not compatible with FreqAI. - It's not possible to hyperopt indicators in the `populate_any_indicators()` function. This means that you 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 instructions also apply to hyperopt. -The best method for combining hyperopt and `FreqAI` is to focus on hyperopting entry/exit thresholds/criteria. You need to focus on hyperopting parameters that are not used in your features. For example, you should not try to hyperopt rolling window lengths in the feature creation, or any part of the `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. +The best method for combining hyperopt and FreqAI is to focus on hyperopting entry/exit thresholds/criteria. You need to focus on hyperopting parameters that are not used in your features. For example, you should not try to hyperopt rolling window lengths in the feature creation, or any part of the 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 threshold for the [Dissimilarity Index (DI)](freqai-feature-engineering.md#identifying-outliers-with-the-dissimilarity-index-di) `DI_values` beyond which we consider data points as outliers: +A good example of a hyperoptable parameter in FreqAI is a threshold for the [Dissimilarity Index (DI)](freqai-feature-engineering.md#identifying-outliers-with-the-dissimilarity-index-di) `DI_values` beyond which we consider data points as outliers: ```python di_max = IntParameter(low=1, high=20, default=10, space='buy', optimize=True, load=True) diff --git a/docs/freqai.md b/docs/freqai.md index 5ab019ca7..b7f0fe21a 100644 --- a/docs/freqai.md +++ b/docs/freqai.md @@ -1,10 +1,10 @@ ![freqai-logo](assets/freqai_doc_logo.svg) -# `FreqAI` +# FreqAI ## Introduction -`FreqAI` is a software designed to automate a variety of tasks associated with training a predictive machine learning model to generate market forecasts given a set of input features. +FreqAI is a software designed to automate a variety of tasks associated with training a predictive machine learning model to generate market forecasts given a set of input features. Features include: @@ -23,7 +23,7 @@ Features include: ## Quick start -The easiest way to quickly test `FreqAI` is to run it in dry mode with the following command: +The easiest way to quickly test FreqAI is to run it in dry mode with the following command: ```bash freqtrade trade --config config_examples/config_freqai.example.json --strategy FreqaiExampleStrategy --freqaimodel LightGBMRegressor --strategy-path freqtrade/templates @@ -37,7 +37,7 @@ An example strategy, prediction model, and config to use as a starting points ca ## General approach -You provide `FreqAI` with a set of custom *base indicators* (the same way as in a [typical Freqtrade strategy](strategy-customization.md)) as well as target values (*labels*). For each pair in the whitelist, `FreqAI` trains a model to predict the target values based on the input of custom indicators. The models are then consistently retrained, with a predetermined frequency, to adapt to market conditions. `FreqAI` offers the ability to both backtest strategies (emulating reality with periodic retraining on historic data) and deploy dry/live runs. In dry/live conditions, `FreqAI` can be set to constant retraining in a background thread to keep models as up to date as possible. +You provide FreqAI with a set of custom *base indicators* (the same way as in a [typical Freqtrade strategy](strategy-customization.md)) as well as target values (*labels*). For each pair in the whitelist, FreqAI trains a model to predict the target values based on the input of custom indicators. The models are then consistently retrained, with a predetermined frequency, to adapt to market conditions. FreqAI offers the ability to both backtest strategies (emulating reality with periodic retraining on historic data) and deploy dry/live runs. In dry/live conditions, FreqAI can be set to constant retraining in a background thread to keep models as up to date as possible. An overview of the algorithm, explaining the data processing pipeline and model usage, is shown below. @@ -45,11 +45,11 @@ An overview of the algorithm, explaining the data processing pipeline and model ### Important machine learning vocabulary -**Features** - the parameters, based on historic data, on which a model is trained. All features for a single candle are stored as a vector. In `FreqAI`, you build a feature data set from anything you can construct in the strategy. +**Features** - the parameters, based on historic data, on which a model is trained. All features for a single candle are stored as a vector. In FreqAI, you build a feature data set from anything you can construct in the strategy. **Labels** - the target values that the model is trained toward. Each feature vector is associated with a single label that is defined by you within the strategy. These labels intentionally look into the future and are what you are training the model to be able to predict. -**Training** - the process of "teaching" the model to match the feature sets to the associated labels. Different types of models "learn" in different ways which means that one might be better than another for a specific application. More information about the different models that are already implemented in `FreqAI` can be found [here](freqai-configuration.md#using-different-prediction-models). +**Training** - the process of "teaching" the model to match the feature sets to the associated labels. Different types of models "learn" in different ways which means that one might be better than another for a specific application. More information about the different models that are already implemented in FreqAI can be found [here](freqai-configuration.md#using-different-prediction-models). **Train data** - a subset of the feature data set that is fed to the model during training to "teach" the model how to predict the targets. This data directly influences weight connections in the model. @@ -59,7 +59,7 @@ An overview of the algorithm, explaining the data processing pipeline and model ## Install prerequisites -The normal Freqtrade install process will ask if you wish to install `FreqAI` dependencies. You should reply "yes" to this question if you wish to use `FreqAI`. If you did not reply yes, you can manually install these dependencies after the install with: +The normal Freqtrade install process will ask if you wish to install FreqAI dependencies. You should reply "yes" to this question if you wish to use FreqAI. If you did not reply yes, you can manually install these dependencies after the install with: ``` bash pip install -r requirements-freqai.txt @@ -70,18 +70,18 @@ pip install -r requirements-freqai.txt ### Usage with docker -If you are using docker, a dedicated tag with `FreqAI` dependencies is available as `:freqai`. As such - you can replace the image line in your docker-compose file with `image: freqtradeorg/freqtrade:develop_freqai`. This image contains the regular `FreqAI` dependencies. Similar to native installs, Catboost will not be available on ARM based devices. +If you are using docker, a dedicated tag with FreqAI dependencies is available as `:freqai`. As such - you can replace the image line in your docker-compose file with `image: freqtradeorg/freqtrade:develop_freqai`. This image contains the regular FreqAI dependencies. Similar to native installs, Catboost will not be available on ARM based devices. ## Common pitfalls -`FreqAI` cannot be combined with dynamic `VolumePairlists` (or any pairlist filter that adds and removes pairs dynamically). -This is for performance reasons - `FreqAI` relies on making quick predictions/retrains. To do this effectively, -it needs to download all the training data at the beginning of a dry/live instance. `FreqAI` stores and appends -new candles automatically for future retrains. This means that if new pairs arrive later in the dry run due to a volume pairlist, it will not have the data ready. However, `FreqAI` does work with the `ShufflePairlist` or a `VolumePairlist` which keeps the total pairlist constant (but reorders the pairs according to volume). +FreqAI cannot be combined with dynamic `VolumePairlists` (or any pairlist filter that adds and removes pairs dynamically). +This is for performance reasons - FreqAI relies on making quick predictions/retrains. To do this effectively, +it needs to download all the training data at the beginning of a dry/live instance. FreqAI stores and appends +new candles automatically for future retrains. This means that if new pairs arrive later in the dry run due to a volume pairlist, it will not have the data ready. However, FreqAI does work with the `ShufflePairlist` or a `VolumePairlist` which keeps the total pairlist constant (but reorders the pairs according to volume). ## Credits -`FreqAI` is developed by a group of individuals who all contribute specific skillsets to the project. +FreqAI is developed by a group of individuals who all contribute specific skillsets to the project. Conception and software development: Robert Caulk @robcaulk From 265795824b99fd997e5e21e6be2b3862f01fa461 Mon Sep 17 00:00:00 2001 From: Robert Caulk Date: Mon, 3 Oct 2022 11:58:22 +0200 Subject: [PATCH 087/126] make default type for close_price and date_pred np.nan --- freqtrade/freqai/data_drawer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqai/data_drawer.py b/freqtrade/freqai/data_drawer.py index 603c477a0..d6dbe8c6c 100644 --- a/freqtrade/freqai/data_drawer.py +++ b/freqtrade/freqai/data_drawer.py @@ -297,8 +297,8 @@ class FreqaiDataDrawer: # this logic carries users between version without needing to # change their identifier if 'close_price' not in df.columns: - df['close_price'] = 0 - df['date_pred'] = 0 + df['close_price'] = np.nan + df['date_pred'] = np.nan df['close_price'].iloc[-1] = strat_df['close'].iloc[-1] df['date_pred'].iloc[-1] = strat_df['date'].iloc[-1] From 3585742b43db7eddfb3cd05f580a763fa24c6bfd Mon Sep 17 00:00:00 2001 From: Robert Caulk Date: Mon, 3 Oct 2022 17:28:45 +0200 Subject: [PATCH 088/126] remove trailing whitespace --- freqtrade/freqai/data_drawer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqai/data_drawer.py b/freqtrade/freqai/data_drawer.py index d6dbe8c6c..cde72bfb5 100644 --- a/freqtrade/freqai/data_drawer.py +++ b/freqtrade/freqai/data_drawer.py @@ -294,7 +294,7 @@ class FreqaiDataDrawer: for return_str in rets: df[return_str].iloc[-1] = rets[return_str] - # this logic carries users between version without needing to + # this logic carries users between version without needing to # change their identifier if 'close_price' not in df.columns: df['close_price'] = np.nan From ca22d857b7369c868214ffea456b322b159e7da2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 3 Oct 2022 18:09:53 +0200 Subject: [PATCH 089/126] Improve handling of trades that fail to cancel as they are closed --- freqtrade/freqtradebot.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 2b20e40fd..4ec9c34ce 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1444,9 +1444,14 @@ class FreqtradeBot(LoggingMixin): trade.close_profit = None trade.close_profit_abs = None # Set exit_reason for fill message + exit_reason_prev = trade.exit_reason trade.exit_reason = trade.exit_reason + f", {reason}" if trade.exit_reason else reason self.update_trade_state(trade, trade.open_order_id, co) - trade.exit_reason = None + # Order might be filled above in odd timing issues. + if co.get('status') in ('canceled', 'cancelled'): + trade.exit_reason = None + else: + trade.exit_reason = exit_reason_prev logger.info(f'{trade.exit_side.capitalize()} order {reason} for {trade}.') cancelled = True From 7f475e37d7cf788b1ec822c405b7d367bd8c17e5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 3 Oct 2022 20:00:56 +0200 Subject: [PATCH 090/126] refactor refresh_latest_ohlcv --- freqtrade/exchange/exchange.py | 54 +++++++++++++++++++++------------- 1 file changed, 34 insertions(+), 20 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 5648d8716..aba149496 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1870,6 +1870,38 @@ class Exchange: return self._async_get_candle_history( pair, timeframe, since_ms=since_ms, candle_type=candle_type) + def _build_ohlcv_dl_jobs( + self, pair_list: ListPairsWithTimeframes, since_ms: Optional[int], + cache: bool) -> Tuple[List[Coroutine], List[Tuple[str, str, CandleType]]]: + """ + Build Coroutines to execute as part of refresh_latest_ohlcv + """ + input_coroutines = [] + cached_pairs = [] + for pair, timeframe, candle_type in set(pair_list): + if ( + timeframe not in self.timeframes + and candle_type in (CandleType.SPOT, CandleType.FUTURES) + ): + logger.warning( + f"Cannot download ({pair}, {timeframe}) combination as this timeframe is " + f"not available on {self.name}. Available timeframes are " + f"{', '.join(self.timeframes)}.") + continue + + if ((pair, timeframe, candle_type) not in self._klines or not cache + or self._now_is_time_to_refresh(pair, timeframe, candle_type)): + input_coroutines.append(self._build_coroutine( + pair, timeframe, candle_type=candle_type, since_ms=since_ms)) + + else: + logger.debug( + f"Using cached candle (OHLCV) data for {pair}, {timeframe}, {candle_type} ..." + ) + cached_pairs.append((pair, timeframe, candle_type)) + + return input_coroutines, cached_pairs + def refresh_latest_ohlcv(self, pair_list: ListPairsWithTimeframes, *, since_ms: Optional[int] = None, cache: bool = True, drop_incomplete: Optional[bool] = None @@ -1887,27 +1919,9 @@ class Exchange: """ logger.debug("Refreshing candle (OHLCV) data for %d pairs", len(pair_list)) drop_incomplete = self._ohlcv_partial_candle if drop_incomplete is None else drop_incomplete - input_coroutines = [] - cached_pairs = [] - # Gather coroutines to run - for pair, timeframe, candle_type in set(pair_list): - if (timeframe not in self.timeframes - and candle_type in (CandleType.SPOT, CandleType.FUTURES)): - logger.warning( - f"Cannot download ({pair}, {timeframe}) combination as this timeframe is " - f"not available on {self.name}. Available timeframes are " - f"{', '.join(self.timeframes)}.") - continue - if ((pair, timeframe, candle_type) not in self._klines or not cache - or self._now_is_time_to_refresh(pair, timeframe, candle_type)): - input_coroutines.append(self._build_coroutine( - pair, timeframe, candle_type=candle_type, since_ms=since_ms)) - else: - logger.debug( - f"Using cached candle (OHLCV) data for {pair}, {timeframe}, {candle_type} ..." - ) - cached_pairs.append((pair, timeframe, candle_type)) + # Gather coroutines to run + input_coroutines, cached_pairs = self._build_ohlcv_dl_jobs(pair_list, since_ms, cache) results_df = {} # Chunk requests into batches of 100 to avoid overwelming ccxt Throttling From 7f308c5186e76381ec76cbc82a0c568e1725aa87 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 4 Oct 2022 06:42:04 +0200 Subject: [PATCH 091/126] Remove last occurance of timerange index --- tests/data/test_history.py | 2 +- tests/test_plotting.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/data/test_history.py b/tests/data/test_history.py index e7e3d4063..b985666cc 100644 --- a/tests/data/test_history.py +++ b/tests/data/test_history.py @@ -480,7 +480,7 @@ def test_validate_backtest_data(default_conf, mocker, caplog, testdatadir) -> No default_conf.update({'strategy': CURRENT_TEST_STRATEGY}) strategy = StrategyResolver.load_strategy(default_conf) - timerange = TimeRange('index', 'index', 200, 250) + timerange = TimeRange() data = strategy.advise_all_indicators( load_data( datadir=testdatadir, diff --git a/tests/test_plotting.py b/tests/test_plotting.py index 52e96e477..f13bdee13 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -63,7 +63,7 @@ def test_init_plotscript(default_conf, mocker, testdatadir): def test_add_indicators(default_conf, testdatadir, caplog): pair = "UNITTEST/BTC" - timerange = TimeRange(None, 'line', 0, -1000) + timerange = TimeRange() data = history.load_pair_history(pair=pair, timeframe='1m', datadir=testdatadir, timerange=timerange) From bc6729f724b4d8ef2f39d7b51c224c252a79bc41 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 4 Oct 2022 06:53:35 +0200 Subject: [PATCH 092/126] Improve readability of "now_is_time_to_refresh" --- freqtrade/exchange/exchange.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index aba149496..b071c677b 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1962,10 +1962,8 @@ class Exchange: interval_in_sec = timeframe_to_seconds(timeframe) return not ( - (self._pairs_last_refresh_time.get( - (pair, timeframe, candle_type), - 0 - ) + interval_in_sec) >= arrow.utcnow().int_timestamp + (self._pairs_last_refresh_time.get((pair, timeframe, candle_type), 0) + + interval_in_sec) >= arrow.utcnow().int_timestamp ) @retrier_async From 016e438468162af803051964da58cde8051a195c Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 4 Oct 2022 08:37:07 +0000 Subject: [PATCH 093/126] Calculate market-change in hyperopt closes #7532 --- freqtrade/optimize/hyperopt.py | 7 ++++++- tests/optimize/test_hyperopt.py | 7 +++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 162556705..9b16873bb 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -24,6 +24,7 @@ from pandas import DataFrame from freqtrade.constants import DATETIME_PRINT_FORMAT, FTHYPT_FILEVERSION, LAST_BT_RESULT_FN, Config from freqtrade.data.converter import trim_dataframes from freqtrade.data.history import get_timerange +from freqtrade.data.metrics import calculate_market_change from freqtrade.enums import HyperoptState from freqtrade.exceptions import OperationalException from freqtrade.misc import deep_merge_dicts, file_dump_json, plural @@ -111,6 +112,7 @@ class Hyperopt: self.clean_hyperopt() + self.market_change = 0 self.num_epochs_saved = 0 self.current_best_epoch: Optional[Dict[str, Any]] = None @@ -357,7 +359,7 @@ class Hyperopt: strat_stats = generate_strategy_stats( self.pairlist, self.backtesting.strategy.get_strategy_name(), - backtesting_results, min_date, max_date, market_change=0 + backtesting_results, min_date, max_date, market_change=self.market_change ) results_explanation = HyperoptTools.format_results_explanation_string( strat_stats, self.config['stake_currency']) @@ -425,6 +427,9 @@ class Hyperopt: # Trim startup period from analyzed dataframe to get correct dates for output. trimmed = trim_dataframes(preprocessed, self.timerange, self.backtesting.required_startup) self.min_date, self.max_date = get_timerange(trimmed) + if not self.market_change: + self.market_change = calculate_market_change(trimmed, 'close') + # Real trimming will happen as part of backtesting. return preprocessed diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index eaea8aee7..5666ebabc 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -297,6 +297,7 @@ def test_params_no_optimize_details(hyperopt) -> None: def test_start_calls_optimizer(mocker, hyperopt_conf, capsys) -> None: dumper = mocker.patch('freqtrade.optimize.hyperopt.dump') dumper2 = mocker.patch('freqtrade.optimize.hyperopt.Hyperopt._save_result') + mocker.patch('freqtrade.optimize.hyperopt.calculate_market_change', return_value=1.5) mocker.patch('freqtrade.optimize.hyperopt.file_dump_json') mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data', @@ -530,6 +531,7 @@ def test_print_json_spaces_all(mocker, hyperopt_conf, capsys) -> None: dumper = mocker.patch('freqtrade.optimize.hyperopt.dump') dumper2 = mocker.patch('freqtrade.optimize.hyperopt.Hyperopt._save_result') mocker.patch('freqtrade.optimize.hyperopt.file_dump_json') + mocker.patch('freqtrade.optimize.hyperopt.calculate_market_change', return_value=1.5) mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data', MagicMock(return_value=(MagicMock(), None))) @@ -581,6 +583,7 @@ def test_print_json_spaces_default(mocker, hyperopt_conf, capsys) -> None: dumper = mocker.patch('freqtrade.optimize.hyperopt.dump') dumper2 = mocker.patch('freqtrade.optimize.hyperopt.Hyperopt._save_result') mocker.patch('freqtrade.optimize.hyperopt.file_dump_json') + mocker.patch('freqtrade.optimize.hyperopt.calculate_market_change', return_value=1.5) mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data', MagicMock(return_value=(MagicMock(), None))) mocker.patch( @@ -622,6 +625,7 @@ def test_print_json_spaces_default(mocker, hyperopt_conf, capsys) -> None: def test_print_json_spaces_roi_stoploss(mocker, hyperopt_conf, capsys) -> None: dumper = mocker.patch('freqtrade.optimize.hyperopt.dump') dumper2 = mocker.patch('freqtrade.optimize.hyperopt.Hyperopt._save_result') + mocker.patch('freqtrade.optimize.hyperopt.calculate_market_change', return_value=1.5) mocker.patch('freqtrade.optimize.hyperopt.file_dump_json') mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data', MagicMock(return_value=(MagicMock(), None))) @@ -663,6 +667,7 @@ def test_print_json_spaces_roi_stoploss(mocker, hyperopt_conf, capsys) -> None: def test_simplified_interface_roi_stoploss(mocker, hyperopt_conf, capsys) -> None: dumper = mocker.patch('freqtrade.optimize.hyperopt.dump') dumper2 = mocker.patch('freqtrade.optimize.hyperopt.Hyperopt._save_result') + mocker.patch('freqtrade.optimize.hyperopt.calculate_market_change', return_value=1.5) mocker.patch('freqtrade.optimize.hyperopt.file_dump_json') mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data', MagicMock(return_value=(MagicMock(), None))) @@ -736,6 +741,7 @@ def test_simplified_interface_all_failed(mocker, hyperopt_conf, caplog) -> None: def test_simplified_interface_buy(mocker, hyperopt_conf, capsys) -> None: dumper = mocker.patch('freqtrade.optimize.hyperopt.dump') dumper2 = mocker.patch('freqtrade.optimize.hyperopt.Hyperopt._save_result') + mocker.patch('freqtrade.optimize.hyperopt.calculate_market_change', return_value=1.5) mocker.patch('freqtrade.optimize.hyperopt.file_dump_json') mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data', MagicMock(return_value=(MagicMock(), None))) @@ -778,6 +784,7 @@ def test_simplified_interface_buy(mocker, hyperopt_conf, capsys) -> None: def test_simplified_interface_sell(mocker, hyperopt_conf, capsys) -> None: dumper = mocker.patch('freqtrade.optimize.hyperopt.dump') dumper2 = mocker.patch('freqtrade.optimize.hyperopt.Hyperopt._save_result') + mocker.patch('freqtrade.optimize.hyperopt.calculate_market_change', return_value=1.5) mocker.patch('freqtrade.optimize.hyperopt.file_dump_json') mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data', MagicMock(return_value=(MagicMock(), None))) From eb8eebe49225c10d91566b09e4c987124493b9be Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 4 Oct 2022 10:08:58 +0000 Subject: [PATCH 094/126] Reset open_order_id after trade cancel Part of #7526 --- freqtrade/freqtradebot.py | 6 ++++-- tests/test_freqtradebot.py | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 4ec9c34ce..15398ca04 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1389,11 +1389,13 @@ class FreqtradeBot(LoggingMixin): reason += f", {constants.CANCEL_REASON['FULLY_CANCELLED']}" else: self.update_trade_state(trade, trade.open_order_id, corder) + trade.open_order_id = None logger.info(f'{side} Order timeout for {trade}.') else: # update_trade_state (and subsequently recalc_trade_from_orders) will handle updates # to the trade object self.update_trade_state(trade, trade.open_order_id, corder) + trade.open_order_id = None logger.info(f'Partial {trade.entry_side} order timeout for {trade}.') reason += f", {constants.CANCEL_REASON['PARTIALLY_FILLED']}" @@ -1450,6 +1452,7 @@ class FreqtradeBot(LoggingMixin): # Order might be filled above in odd timing issues. if co.get('status') in ('canceled', 'cancelled'): trade.exit_reason = None + trade.open_order_id = None else: trade.exit_reason = exit_reason_prev @@ -1459,8 +1462,7 @@ class FreqtradeBot(LoggingMixin): reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE'] logger.info(f'{trade.exit_side.capitalize()} order {reason} for {trade}.') self.update_trade_state(trade, trade.open_order_id, order) - - self.wallets.update() + trade.open_order_id = None self._notify_exit_cancel( trade, diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index cdea772dc..c127e3850 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -3178,6 +3178,7 @@ def test_handle_cancel_exit_limit(mocker, default_conf_usdt, fee) -> None: assert send_msg_mock.call_count == 1 assert trade.close_rate is None assert trade.exit_reason is None + assert trade.open_order_id is None send_msg_mock.reset_mock() From a6296be2f575c8630de1c5ea3f3cc237676a47c3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 4 Oct 2022 10:27:04 +0000 Subject: [PATCH 095/126] Update market_change datatype --- 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 9b16873bb..d93bbbfc1 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -112,7 +112,7 @@ class Hyperopt: self.clean_hyperopt() - self.market_change = 0 + self.market_change = 0.0 self.num_epochs_saved = 0 self.current_best_epoch: Optional[Dict[str, Any]] = None From 68db0bc647338af421afb603c8345651799759ee Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 4 Oct 2022 18:25:23 +0200 Subject: [PATCH 096/126] move check_exchange to exchange package --- freqtrade/configuration/__init__.py | 1 - freqtrade/configuration/configuration.py | 4 +- .../check_exchange.py | 0 tests/exchange/test_exchange_utils.py | 69 +++++++++++++++++++ tests/test_configuration.py | 63 +---------------- 5 files changed, 73 insertions(+), 64 deletions(-) rename freqtrade/{configuration => exchange}/check_exchange.py (100%) create mode 100644 tests/exchange/test_exchange_utils.py diff --git a/freqtrade/configuration/__init__.py b/freqtrade/configuration/__init__.py index 730a4e47f..dee42d535 100644 --- a/freqtrade/configuration/__init__.py +++ b/freqtrade/configuration/__init__.py @@ -1,6 +1,5 @@ # flake8: noqa: F401 -from freqtrade.configuration.check_exchange import check_exchange from freqtrade.configuration.config_setup import setup_utils_configuration from freqtrade.configuration.config_validation import validate_config_consistency from freqtrade.configuration.configuration import Configuration diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 76105cc4d..5e6da4178 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -8,7 +8,6 @@ from pathlib import Path from typing import Any, Callable, Dict, List, Optional from freqtrade import constants -from freqtrade.configuration.check_exchange import check_exchange from freqtrade.configuration.deprecated_settings import process_temporary_deprecated_settings from freqtrade.configuration.directory_operations import create_datadir, create_userdata_dir from freqtrade.configuration.environment_vars import enironment_vars_to_dict @@ -100,6 +99,9 @@ class Configuration: self._process_freqai_options(config) + # Import check_exchange here to avoid import cycle problems + from freqtrade.exchange.check_exchange import check_exchange + # Check if the exchange set by the user is supported check_exchange(config, config.get('experimental', {}).get('block_bad_exchanges', True)) diff --git a/freqtrade/configuration/check_exchange.py b/freqtrade/exchange/check_exchange.py similarity index 100% rename from freqtrade/configuration/check_exchange.py rename to freqtrade/exchange/check_exchange.py diff --git a/tests/exchange/test_exchange_utils.py b/tests/exchange/test_exchange_utils.py new file mode 100644 index 000000000..a454b37d0 --- /dev/null +++ b/tests/exchange/test_exchange_utils.py @@ -0,0 +1,69 @@ +# pragma pylint: disable=missing-docstring, protected-access, invalid-name + +import pytest + +from freqtrade.enums import RunMode +from freqtrade.exceptions import OperationalException +from freqtrade.exchange.check_exchange import check_exchange +from tests.conftest import log_has_re + + +def test_check_exchange(default_conf, caplog) -> None: + # Test an officially supported by Freqtrade team exchange + default_conf['runmode'] = RunMode.DRY_RUN + default_conf.get('exchange').update({'name': 'BITTREX'}) + assert check_exchange(default_conf) + assert log_has_re(r"Exchange .* is officially supported by the Freqtrade development team\.", + caplog) + caplog.clear() + + # Test an officially supported by Freqtrade team exchange + default_conf.get('exchange').update({'name': 'binance'}) + assert check_exchange(default_conf) + assert log_has_re(r"Exchange .* is officially supported by the Freqtrade development team\.", + caplog) + caplog.clear() + + # Test an available exchange, supported by ccxt + default_conf.get('exchange').update({'name': 'huobipro'}) + assert check_exchange(default_conf) + assert log_has_re(r"Exchange .* is known to the the ccxt library, available for the bot, " + r"but not officially supported " + r"by the Freqtrade development team\. .*", caplog) + caplog.clear() + + # Test a 'bad' exchange, which known to have serious problems + default_conf.get('exchange').update({'name': 'bitmex'}) + with pytest.raises(OperationalException, + match=r"Exchange .* will not work with Freqtrade\..*"): + check_exchange(default_conf) + caplog.clear() + + # Test a 'bad' exchange with check_for_bad=False + default_conf.get('exchange').update({'name': 'bitmex'}) + assert check_exchange(default_conf, False) + assert log_has_re(r"Exchange .* is known to the the ccxt library, available for the bot, " + r"but not officially supported " + r"by the Freqtrade development team\. .*", caplog) + caplog.clear() + + # Test an invalid exchange + default_conf.get('exchange').update({'name': 'unknown_exchange'}) + with pytest.raises( + OperationalException, + match=r'Exchange "unknown_exchange" is not known to the ccxt library ' + r'and therefore not available for the bot.*' + ): + check_exchange(default_conf) + + # Test no exchange... + default_conf.get('exchange').update({'name': ''}) + default_conf['runmode'] = RunMode.PLOT + assert check_exchange(default_conf) + + # Test no exchange... + default_conf.get('exchange').update({'name': ''}) + default_conf['runmode'] = RunMode.UTIL_EXCHANGE + with pytest.raises(OperationalException, + match=r'This command requires a configured exchange.*'): + check_exchange(default_conf) diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 99edf0233..2336e3585 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -11,7 +11,7 @@ import pytest from jsonschema import ValidationError from freqtrade.commands import Arguments -from freqtrade.configuration import Configuration, check_exchange, validate_config_consistency +from freqtrade.configuration import Configuration, validate_config_consistency from freqtrade.configuration.config_validation import validate_config_schema from freqtrade.configuration.deprecated_settings import (check_conflicting_settings, process_deprecated_setting, @@ -584,67 +584,6 @@ def test_hyperopt_with_arguments(mocker, default_conf, caplog) -> None: assert config['runmode'] == RunMode.HYPEROPT -def test_check_exchange(default_conf, caplog) -> None: - # Test an officially supported by Freqtrade team exchange - default_conf['runmode'] = RunMode.DRY_RUN - default_conf.get('exchange').update({'name': 'BITTREX'}) - assert check_exchange(default_conf) - assert log_has_re(r"Exchange .* is officially supported by the Freqtrade development team\.", - caplog) - caplog.clear() - - # Test an officially supported by Freqtrade team exchange - default_conf.get('exchange').update({'name': 'binance'}) - assert check_exchange(default_conf) - assert log_has_re(r"Exchange .* is officially supported by the Freqtrade development team\.", - caplog) - caplog.clear() - - # Test an available exchange, supported by ccxt - default_conf.get('exchange').update({'name': 'huobipro'}) - assert check_exchange(default_conf) - assert log_has_re(r"Exchange .* is known to the the ccxt library, available for the bot, " - r"but not officially supported " - r"by the Freqtrade development team\. .*", caplog) - caplog.clear() - - # Test a 'bad' exchange, which known to have serious problems - default_conf.get('exchange').update({'name': 'bitmex'}) - with pytest.raises(OperationalException, - match=r"Exchange .* will not work with Freqtrade\..*"): - check_exchange(default_conf) - caplog.clear() - - # Test a 'bad' exchange with check_for_bad=False - default_conf.get('exchange').update({'name': 'bitmex'}) - assert check_exchange(default_conf, False) - assert log_has_re(r"Exchange .* is known to the the ccxt library, available for the bot, " - r"but not officially supported " - r"by the Freqtrade development team\. .*", caplog) - caplog.clear() - - # Test an invalid exchange - default_conf.get('exchange').update({'name': 'unknown_exchange'}) - with pytest.raises( - OperationalException, - match=r'Exchange "unknown_exchange" is not known to the ccxt library ' - r'and therefore not available for the bot.*' - ): - check_exchange(default_conf) - - # Test no exchange... - default_conf.get('exchange').update({'name': ''}) - default_conf['runmode'] = RunMode.PLOT - assert check_exchange(default_conf) - - # Test no exchange... - default_conf.get('exchange').update({'name': ''}) - default_conf['runmode'] = RunMode.UTIL_EXCHANGE - with pytest.raises(OperationalException, - match=r'This command requires a configured exchange.*'): - check_exchange(default_conf) - - def test_cli_verbose_with_params(default_conf, mocker, caplog) -> None: patched_configuration_load_config_file(mocker, default_conf) From c1d8ade2fa0fce58b49ee7324b27c4875ff5f008 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 4 Oct 2022 19:28:57 +0200 Subject: [PATCH 097/126] Improve supported exchange check by supporting exchange aliases --- freqtrade/exchange/__init__.py | 4 ++-- freqtrade/exchange/check_exchange.py | 6 +++--- freqtrade/exchange/exchange.py | 7 +------ tests/exchange/test_exchange_utils.py | 20 ++++++++++++++++++-- 4 files changed, 24 insertions(+), 13 deletions(-) diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index ff7ec7e04..1b5ca11ee 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -12,8 +12,8 @@ from freqtrade.exchange.coinbasepro import Coinbasepro from freqtrade.exchange.exchange import (amount_to_contract_precision, amount_to_contracts, amount_to_precision, available_exchanges, ccxt_exchanges, contracts_to_amount, date_minus_candles, - is_exchange_known_ccxt, is_exchange_officially_supported, - market_is_active, price_to_precision, timeframe_to_minutes, + is_exchange_known_ccxt, market_is_active, + price_to_precision, timeframe_to_minutes, timeframe_to_msecs, timeframe_to_next_date, timeframe_to_prev_date, timeframe_to_seconds, validate_exchange, validate_exchanges) diff --git a/freqtrade/exchange/check_exchange.py b/freqtrade/exchange/check_exchange.py index c3d859275..69330bcd0 100644 --- a/freqtrade/exchange/check_exchange.py +++ b/freqtrade/exchange/check_exchange.py @@ -3,8 +3,8 @@ import logging from freqtrade.constants import Config from freqtrade.enums import RunMode from freqtrade.exceptions import OperationalException -from freqtrade.exchange import (available_exchanges, is_exchange_known_ccxt, - is_exchange_officially_supported, validate_exchange) +from freqtrade.exchange import available_exchanges, is_exchange_known_ccxt, validate_exchange +from freqtrade.exchange.common import MAP_EXCHANGE_CHILDCLASS, SUPPORTED_EXCHANGES logger = logging.getLogger(__name__) @@ -52,7 +52,7 @@ def check_exchange(config: Config, check_for_bad: bool = True) -> bool: else: logger.warning(f'Exchange "{exchange}" will not work with Freqtrade. Reason: {reason}') - if is_exchange_officially_supported(exchange): + if MAP_EXCHANGE_CHILDCLASS.get(exchange, exchange) in SUPPORTED_EXCHANGES: logger.info(f'Exchange "{exchange}" is officially supported ' f'by the Freqtrade development team.') else: diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index b071c677b..cb9cbebbd 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -30,8 +30,7 @@ from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFun RetryableOrderError, TemporaryError) from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, BAD_EXCHANGES, EXCHANGE_HAS_OPTIONAL, EXCHANGE_HAS_REQUIRED, - SUPPORTED_EXCHANGES, remove_credentials, retrier, - retrier_async) + remove_credentials, retrier, retrier_async) from freqtrade.misc import (chunks, deep_merge_dicts, file_dump_json, file_load_json, safe_value_fallback2) from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist @@ -2773,10 +2772,6 @@ def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = Non return exchange_name in ccxt_exchanges(ccxt_module) -def is_exchange_officially_supported(exchange_name: str) -> bool: - return exchange_name in SUPPORTED_EXCHANGES - - def ccxt_exchanges(ccxt_module: CcxtModuleType = None) -> List[str]: """ Return the list of all exchanges known to ccxt diff --git a/tests/exchange/test_exchange_utils.py b/tests/exchange/test_exchange_utils.py index a454b37d0..db206ab98 100644 --- a/tests/exchange/test_exchange_utils.py +++ b/tests/exchange/test_exchange_utils.py @@ -20,10 +20,26 @@ def test_check_exchange(default_conf, caplog) -> None: # Test an officially supported by Freqtrade team exchange default_conf.get('exchange').update({'name': 'binance'}) assert check_exchange(default_conf) - assert log_has_re(r"Exchange .* is officially supported by the Freqtrade development team\.", - caplog) + assert log_has_re( + r"Exchange \"binance\" is officially supported by the Freqtrade development team\.", + caplog) caplog.clear() + # Test an officially supported by Freqtrade team exchange + default_conf.get('exchange').update({'name': 'binanceus'}) + assert check_exchange(default_conf) + assert log_has_re( + r"Exchange \"binanceus\" is officially supported by the Freqtrade development team\.", + caplog) + caplog.clear() + + # Test an officially supported by Freqtrade team exchange - with remapping + default_conf.get('exchange').update({'name': 'okex'}) + assert check_exchange(default_conf) + assert log_has_re( + r"Exchange \"okex\" is officially supported by the Freqtrade development team\.", + caplog) + caplog.clear() # Test an available exchange, supported by ccxt default_conf.get('exchange').update({'name': 'huobipro'}) assert check_exchange(default_conf) From 3264d7b8900c3d9d955248a212009f1dbd5642c4 Mon Sep 17 00:00:00 2001 From: Marek Cieplucha Date: Tue, 4 Oct 2022 20:27:13 +0200 Subject: [PATCH 098/126] Fix for #7534 in backtesting --- freqtrade/optimize/backtesting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 376c2de7c..83159dfe4 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -1045,7 +1045,7 @@ class Backtesting: if requested_rate: self._enter_trade(pair=trade.pair, row=row, trade=trade, requested_rate=requested_rate, - requested_stake=(order.remaining * order.price), + requested_stake=(order.remaining * order.price / trade.leverage), direction='short' if trade.is_short else 'long') self.replaced_entry_orders += 1 else: From 5019300d5c1752e71bfb360fe5a26e42c8aca26c Mon Sep 17 00:00:00 2001 From: Marek Cieplucha Date: Tue, 4 Oct 2022 20:28:47 +0200 Subject: [PATCH 099/126] Fix for #7534 in bot --- freqtrade/freqtradebot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 15398ca04..213bc6157 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1311,7 +1311,7 @@ class FreqtradeBot(LoggingMixin): # place new order only if new price is supplied self.execute_entry( pair=trade.pair, - stake_amount=(order_obj.remaining * order_obj.price), + stake_amount=(order_obj.remaining * order_obj.price / trade.leverage) price=adjusted_entry_price, trade=trade, is_short=trade.is_short, From 4df533feb0c806a729390035e1d990febc90996a Mon Sep 17 00:00:00 2001 From: Marek Cieplucha Date: Tue, 4 Oct 2022 21:16:30 +0200 Subject: [PATCH 100/126] Add missing comma --- freqtrade/freqtradebot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 213bc6157..cd111679c 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1311,7 +1311,7 @@ class FreqtradeBot(LoggingMixin): # place new order only if new price is supplied self.execute_entry( pair=trade.pair, - stake_amount=(order_obj.remaining * order_obj.price / trade.leverage) + stake_amount=(order_obj.remaining * order_obj.price / trade.leverage), price=adjusted_entry_price, trade=trade, is_short=trade.is_short, From ca913fb29d02dc7c8492f926ab1f6268b6812e5f Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 5 Oct 2022 07:28:34 +0200 Subject: [PATCH 101/126] Add leveraged test-case for order-adjustment --- tests/test_integration.py | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/tests/test_integration.py b/tests/test_integration.py index a848de5d3..f2504c23a 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -351,8 +351,13 @@ def test_dca_short(default_conf_usdt, ticker_usdt, fee, mocker) -> None: assert trade.nr_of_successful_exits == 1 -def test_dca_order_adjust(default_conf_usdt, ticker_usdt, fee, mocker) -> None: +@pytest.mark.parametrize('leverage', [ + 1, 2 +]) +def test_dca_order_adjust(default_conf_usdt, ticker_usdt, leverage, fee, mocker) -> None: default_conf_usdt['position_adjustment_enable'] = True + default_conf_usdt['trading_mode'] = 'futures' + default_conf_usdt['margin_mode'] = 'isolated' freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) mocker.patch.multiple( @@ -363,9 +368,14 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, fee, mocker) -> None: price_to_precision=lambda s, x, y: y, ) mocker.patch('freqtrade.exchange.Exchange._is_dry_limit_order_filled', return_value=False) + mocker.patch("freqtrade.exchange.Exchange.get_max_leverage", return_value=10) + mocker.patch("freqtrade.exchange.Exchange.get_funding_fees", return_value=0) + mocker.patch("freqtrade.exchange.Exchange.get_maintenance_ratio_and_amt", return_value=(0, 0)) patch_get_signal(freqtrade) freqtrade.strategy.custom_entry_price = lambda **kwargs: ticker_usdt['ask'] * 0.96 + freqtrade.strategy.leverage = MagicMock(return_value=leverage) + freqtrade.strategy.minimal_roi = {0: 0.2} freqtrade.enter_positions() @@ -377,6 +387,8 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, fee, mocker) -> None: assert trade.open_rate == 1.96 assert trade.stop_loss_pct is None assert trade.stop_loss == 0.0 + assert trade.leverage == leverage + assert trade.stake_amount == 60 assert trade.initial_stop_loss == 0.0 assert trade.initial_stop_loss_pct is None # No adjustment @@ -396,6 +408,7 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, fee, mocker) -> None: assert trade.open_rate == 1.96 assert trade.stop_loss_pct is None assert trade.stop_loss == 0.0 + assert trade.stake_amount == 60 assert trade.initial_stop_loss == 0.0 assert trade.initial_stop_loss_pct is None @@ -407,9 +420,10 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, fee, mocker) -> None: assert trade.open_order_id is None # Open rate is not adjusted yet assert trade.open_rate == 1.99 + assert trade.stake_amount == 60 assert trade.stop_loss_pct == -0.1 - assert trade.stop_loss == 1.99 * 0.9 - assert trade.initial_stop_loss == 1.99 * 0.9 + assert pytest.approx(trade.stop_loss) == 1.99 * (1 - 0.1 / leverage) + assert pytest.approx(trade.initial_stop_loss) == 1.99 * (1 - 0.1 / leverage) assert trade.initial_stop_loss_pct == -0.1 # 2nd order - not filling @@ -422,7 +436,7 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, fee, mocker) -> None: assert trade.open_order_id is not None assert trade.open_rate == 1.99 assert trade.orders[-1].price == 1.96 - assert trade.orders[-1].cost == 120 + assert trade.orders[-1].cost == 120 * leverage # Replace new order with diff. order at a lower price freqtrade.strategy.adjust_entry_price = MagicMock(return_value=1.95) @@ -432,8 +446,9 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, fee, mocker) -> None: assert len(trade.orders) == 4 assert trade.open_order_id is not None assert trade.open_rate == 1.99 + assert trade.stake_amount == 60 assert trade.orders[-1].price == 1.95 - assert pytest.approx(trade.orders[-1].cost) == 120 + assert pytest.approx(trade.orders[-1].cost) == 120 * leverage # Fill DCA order freqtrade.strategy.adjust_trade_position = MagicMock(return_value=None) @@ -446,13 +461,13 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, fee, mocker) -> None: assert trade.open_order_id is None assert pytest.approx(trade.open_rate) == 1.963153456 assert trade.orders[-1].price == 1.95 - assert pytest.approx(trade.orders[-1].cost) == 120 + assert pytest.approx(trade.orders[-1].cost) == 120 * leverage assert trade.orders[-1].status == 'closed' - assert pytest.approx(trade.amount) == 91.689215 + assert pytest.approx(trade.amount) == 91.689215 * leverage # Check the 2 filled orders equal the above amount - assert pytest.approx(trade.orders[1].amount) == 30.150753768 - assert pytest.approx(trade.orders[-1].amount) == 61.538461232 + assert pytest.approx(trade.orders[1].amount) == 30.150753768 * leverage + assert pytest.approx(trade.orders[-1].amount) == 61.538461232 * leverage @pytest.mark.parametrize('leverage', [1, 2]) From 0e0bda8f130488b7de38e563e483b92b4649a6f1 Mon Sep 17 00:00:00 2001 From: robcaulk Date: Wed, 5 Oct 2022 14:08:03 +0200 Subject: [PATCH 102/126] improve freqai tests --- tests/freqai/conftest.py | 7 +- tests/freqai/test_freqai_datadrawer.py | 2 +- tests/freqai/test_freqai_datakitchen.py | 10 +-- tests/freqai/test_freqai_interface.py | 88 +++++++++++++++++++++---- 4 files changed, 87 insertions(+), 20 deletions(-) diff --git a/tests/freqai/conftest.py b/tests/freqai/conftest.py index 2c6210a0e..026b45afc 100644 --- a/tests/freqai/conftest.py +++ b/tests/freqai/conftest.py @@ -29,15 +29,16 @@ def freqai_conf(default_conf, tmpdir): "enabled": True, "startup_candles": 10000, "purge_old_models": True, - "train_period_days": 5, + "train_period_days": 2, "backtest_period_days": 2, "live_retrain_hours": 0, "expiration_hours": 1, "identifier": "uniqe-id100", "live_trained_timestamp": 0, + "data_kitchen_thread_count": 2, "feature_parameters": { "include_timeframes": ["5m"], - "include_corr_pairlist": ["ADA/BTC", "DASH/BTC"], + "include_corr_pairlist": ["ADA/BTC"], "label_period_candles": 20, "include_shifted_candles": 1, "DI_threshold": 0.9, @@ -47,7 +48,7 @@ def freqai_conf(default_conf, tmpdir): "stratify_training_data": 0, "indicator_periods_candles": [10], }, - "data_split_parameters": {"test_size": 0.33, "random_state": 1}, + "data_split_parameters": {"test_size": 0.33, "shuffle": False}, "model_training_parameters": {"n_estimators": 100}, }, "config_files": [Path('config_examples', 'config_freqai.example.json')] diff --git a/tests/freqai/test_freqai_datadrawer.py b/tests/freqai/test_freqai_datadrawer.py index a6df60e61..1d1c44a1e 100644 --- a/tests/freqai/test_freqai_datadrawer.py +++ b/tests/freqai/test_freqai_datadrawer.py @@ -90,5 +90,5 @@ def test_use_strategy_to_populate_indicators(mocker, freqai_conf): df = freqai.dk.use_strategy_to_populate_indicators(strategy, corr_df, base_df, 'LTC/BTC') - assert len(df.columns) == 45 + assert len(df.columns) == 33 shutil.rmtree(Path(freqai.dk.full_path)) diff --git a/tests/freqai/test_freqai_datakitchen.py b/tests/freqai/test_freqai_datakitchen.py index b99ac236d..023193818 100644 --- a/tests/freqai/test_freqai_datakitchen.py +++ b/tests/freqai/test_freqai_datakitchen.py @@ -71,14 +71,14 @@ def test_use_DBSCAN_to_remove_outliers(mocker, freqai_conf, caplog): freqai = make_data_dictionary(mocker, freqai_conf) # freqai_conf['freqai']['feature_parameters'].update({"outlier_protection_percentage": 1}) freqai.dk.use_DBSCAN_to_remove_outliers(predict=False) - assert log_has_re(r"DBSCAN found eps of 2\.3\d\.", caplog) + assert log_has_re(r"DBSCAN found eps of 1.75", caplog) 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.54 + assert round(avg_mean_dist, 2) == 1.99 def test_use_SVM_to_remove_outliers_and_outlier_protection(mocker, freqai_conf, caplog): @@ -86,7 +86,7 @@ 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.66%", + "SVM detected 7.36%", caplog, ) @@ -125,7 +125,7 @@ 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 + assert len(freqai.dk.data) == 32 def test_filter_features(mocker, freqai_conf): @@ -139,7 +139,7 @@ def test_filter_features(mocker, freqai_conf): training_filter=True, ) - assert len(filtered_df.columns) == 26 + assert len(filtered_df.columns) == 14 def test_make_train_test_datasets(mocker, freqai_conf): diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index 4512a43f0..7921659bc 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -7,10 +7,14 @@ import pytest from freqtrade.configuration import TimeRange from freqtrade.data.dataprovider import DataProvider +from freqtrade.enums import RunMode from freqtrade.freqai.data_kitchen import FreqaiDataKitchen +from freqtrade.freqai.utils import download_all_data_for_training, get_required_data_timerange +from freqtrade.persistence import Trade from freqtrade.plugins.pairlistmanager import PairListManager from tests.conftest import get_patched_exchange, log_has_re from tests.freqai.conftest import get_patched_freqai_strategy +from freqtrade.optimize.backtesting import Backtesting def is_arm() -> bool: @@ -18,15 +22,21 @@ def is_arm() -> bool: return "arm" in machine or "aarch64" in machine +def is_mac() -> bool: + machine = platform.system() + return "Darwin" in machine + + @pytest.mark.parametrize('model', [ 'LightGBMRegressor', 'XGBoostRegressor', 'CatboostRegressor', ]) -def test_extract_data_and_train_model_Regressors(mocker, freqai_conf, model): +def test_extract_data_and_train_model_Standard(mocker, freqai_conf, model): if is_arm() and model == 'CatboostRegressor': pytest.skip("CatBoost is not supported on ARM") + model_save_ext = 'joblib' freqai_conf.update({"freqaimodel": model}) freqai_conf.update({"timerange": "20180110-20180130"}) freqai_conf.update({"strategy": "freqai_test_strat"}) @@ -43,16 +53,16 @@ def test_extract_data_and_train_model_Regressors(mocker, freqai_conf, model): freqai.dd.pair_dict = MagicMock() - data_load_timerange = TimeRange.parse_timerange("20180110-20180130") - new_timerange = TimeRange.parse_timerange("20180120-20180130") + data_load_timerange = TimeRange.parse_timerange("20180125-20180130") + new_timerange = TimeRange.parse_timerange("20180127-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}_model.{model_save_ext}").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)) @@ -92,7 +102,7 @@ def test_extract_data_and_train_model_MultiTargets(mocker, freqai_conf, model): 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 + assert len(freqai.dk.data['training_features_list']) == 14 shutil.rmtree(Path(freqai.dk.full_path)) @@ -136,9 +146,28 @@ def test_extract_data_and_train_model_Classifiers(mocker, freqai_conf, model): shutil.rmtree(Path(freqai.dk.full_path)) -def test_start_backtesting(mocker, freqai_conf): - freqai_conf.update({"timerange": "20180120-20180130"}) +@pytest.mark.parametrize( + "model, num_files, strat", + [ + ("LightGBMRegressor", 6, "freqai_test_strat"), + ("XGBoostRegressor", 6, "freqai_test_strat"), + ("CatboostRegressor", 6, "freqai_test_strat"), + ("XGBoostClassifier", 6, "freqai_test_classifier"), + ("LightGBMClassifier", 6, "freqai_test_classifier"), + ("CatboostClassifier", 6, "freqai_test_classifier") + ], + ) +def test_start_backtesting(mocker, freqai_conf, model, num_files, strat): freqai_conf.get("freqai", {}).update({"save_backtest_models": True}) + freqai_conf['runmode'] = RunMode.BACKTEST + Trade.use_db = False + if is_arm() and "Catboost" in model: + pytest.skip("CatBoost is not supported on ARM") + + freqai_conf.update({"freqaimodel": model}) + freqai_conf.update({"timerange": "20180120-20180130"}) + freqai_conf.update({"strategy": strat}) + strategy = get_patched_freqai_strategy(mocker, freqai_conf) exchange = get_patched_exchange(mocker, freqai_conf) strategy.dp = DataProvider(freqai_conf, exchange) @@ -157,8 +186,8 @@ def test_start_backtesting(mocker, freqai_conf): freqai.start_backtesting(df, metadata, freqai.dk) model_folders = [x for x in freqai.dd.full_path.iterdir() if x.is_dir()] - assert len(model_folders) == 6 - + assert len(model_folders) == num_files + Backtesting.cleanup() shutil.rmtree(Path(freqai.dk.full_path)) @@ -211,7 +240,7 @@ def test_start_backtesting_from_existing_folder(mocker, freqai_conf, caplog): assert len(model_folders) == 6 - # without deleting the exiting folder structure, re-run + # without deleting the existing folder structure, re-run freqai_conf.update({"timerange": "20180120-20180130"}) strategy = get_patched_freqai_strategy(mocker, freqai_conf) @@ -375,3 +404,40 @@ def test_freqai_informative_pairs(mocker, freqai_conf, timeframes, corr_pairs): pairs_b = strategy.gather_informative_pairs() # we expect unique pairs * timeframes assert len(pairs_b) == len(set(pairlist + corr_pairs)) * len(timeframes) + + +def test_start_set_train_queue(mocker, freqai_conf, caplog): + strategy = get_patched_freqai_strategy(mocker, freqai_conf) + exchange = get_patched_exchange(mocker, freqai_conf) + pairlist = PairListManager(exchange, freqai_conf) + strategy.dp = DataProvider(freqai_conf, exchange, pairlist) + strategy.freqai_info = freqai_conf.get("freqai", {}) + freqai = strategy.freqai + freqai.live = False + + freqai.train_queue = freqai._set_train_queue() + + assert log_has_re( + "Set fresh train queue from whitelist.", + caplog, + ) + + +def test_get_required_data_timerange(mocker, freqai_conf): + time_range = get_required_data_timerange(freqai_conf) + assert (time_range.stopts - time_range.startts) == 177300 + + +def test_download_all_data_for_training(mocker, freqai_conf, caplog, tmpdir): + strategy = get_patched_freqai_strategy(mocker, freqai_conf) + exchange = get_patched_exchange(mocker, freqai_conf) + pairlist = PairListManager(exchange, freqai_conf) + strategy.dp = DataProvider(freqai_conf, exchange, pairlist) + freqai_conf['pairs'] = freqai_conf['exchange']['pair_whitelist'] + freqai_conf['datadir'] = Path(tmpdir) + download_all_data_for_training(strategy.dp, freqai_conf) + + assert log_has_re( + "Downloading", + caplog, + ) From 4edb30bfa82bbfeed89eefedb222afd18aec0819 Mon Sep 17 00:00:00 2001 From: robcaulk Date: Wed, 5 Oct 2022 14:11:19 +0200 Subject: [PATCH 103/126] isort --- tests/freqai/test_freqai_interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index 7921659bc..a61853c47 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -10,11 +10,11 @@ from freqtrade.data.dataprovider import DataProvider from freqtrade.enums import RunMode from freqtrade.freqai.data_kitchen import FreqaiDataKitchen from freqtrade.freqai.utils import download_all_data_for_training, get_required_data_timerange +from freqtrade.optimize.backtesting import Backtesting from freqtrade.persistence import Trade from freqtrade.plugins.pairlistmanager import PairListManager from tests.conftest import get_patched_exchange, log_has_re from tests.freqai.conftest import get_patched_freqai_strategy -from freqtrade.optimize.backtesting import Backtesting def is_arm() -> bool: From 0d67afe15b75fc433ef962bb84c8fa8b1672ba2e Mon Sep 17 00:00:00 2001 From: robcaulk Date: Wed, 5 Oct 2022 14:38:50 +0200 Subject: [PATCH 104/126] allow less precision, ensure regex is catching the right chars --- tests/freqai/test_freqai_datakitchen.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/freqai/test_freqai_datakitchen.py b/tests/freqai/test_freqai_datakitchen.py index 023193818..f60b29bf1 100644 --- a/tests/freqai/test_freqai_datakitchen.py +++ b/tests/freqai/test_freqai_datakitchen.py @@ -71,7 +71,7 @@ def test_use_DBSCAN_to_remove_outliers(mocker, freqai_conf, caplog): freqai = make_data_dictionary(mocker, freqai_conf) # freqai_conf['freqai']['feature_parameters'].update({"outlier_protection_percentage": 1}) freqai.dk.use_DBSCAN_to_remove_outliers(predict=False) - assert log_has_re(r"DBSCAN found eps of 1.75", caplog) + assert log_has_re(r"DBSCAN found eps of 1\.7\d\.", caplog) def test_compute_distances(mocker, freqai_conf): From 7dbb78da955463164eabf3eb9fb6107937aca7e6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 5 Oct 2022 13:14:36 +0000 Subject: [PATCH 105/126] Losely pin pydantic to avoid dependency problems closes #7537 --- requirements.txt | 1 + setup.py | 1 + 2 files changed, 2 insertions(+) diff --git a/requirements.txt b/requirements.txt index 3cc830290..4f4b30d0c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -38,6 +38,7 @@ sdnotify==0.3.2 # API Server fastapi==0.85.0 +pydantic>=1.8.0 uvicorn==0.18.3 pyjwt==2.5.0 aiofiles==22.1.0 diff --git a/setup.py b/setup.py index d3f9ea7c0..b3693c9f9 100644 --- a/setup.py +++ b/setup.py @@ -75,6 +75,7 @@ setup( 'joblib>=1.2.0', 'pyarrow; platform_machine != "armv7l"', 'fastapi', + 'pydantic>=1.8.0', 'uvicorn', 'psutil', 'pyjwt', From 9b1fb02df84f2af68ef764c9542511861d41843d Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 5 Oct 2022 18:09:26 +0200 Subject: [PATCH 106/126] Refactor generic data generation to conftest --- tests/conftest.py | 22 ++++++++++++++++++++++ tests/strategy/test_strategy_helpers.py | 25 ++----------------------- 2 files changed, 24 insertions(+), 23 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index a9eeb481e..f87fa59c5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,6 +10,7 @@ from unittest.mock import MagicMock, Mock, PropertyMock import arrow import numpy as np +import pandas as pd import pytest from telegram import Chat, Message, Update @@ -19,6 +20,7 @@ from freqtrade.data.converter import ohlcv_to_dataframe from freqtrade.edge import PairInfo from freqtrade.enums import CandleType, MarginMode, RunMode, SignalDirection, TradingMode from freqtrade.exchange import Exchange +from freqtrade.exchange.exchange import timeframe_to_minutes from freqtrade.freqtradebot import FreqtradeBot from freqtrade.persistence import LocalTrade, Order, Trade, init_db from freqtrade.resolvers import ExchangeResolver @@ -82,6 +84,26 @@ def get_args(args): return Arguments(args).get_parsed_arg() +def generate_test_data(timeframe: str, size: int, start: str = '2020-07-05'): + np.random.seed(42) + tf_mins = timeframe_to_minutes(timeframe) + + base = np.random.normal(20, 2, size=size) + + date = pd.date_range(start, periods=size, freq=f'{tf_mins}min', tz='UTC') + df = pd.DataFrame({ + 'date': date, + 'open': base, + 'high': base + np.random.normal(2, 1, size=size), + 'low': base - np.random.normal(2, 1, size=size), + 'close': base + np.random.normal(0, 1, size=size), + 'volume': np.random.normal(200, size=size) + } + ) + df = df.dropna() + return df + + # Source: https://stackoverflow.com/questions/29881236/how-to-mock-asyncio-coroutines # TODO: This should be replaced with AsyncMock once support for python 3.7 is dropped. def get_mock_coro(return_value=None, side_effect=None): diff --git a/tests/strategy/test_strategy_helpers.py b/tests/strategy/test_strategy_helpers.py index 8cb990e87..f42f9e681 100644 --- a/tests/strategy/test_strategy_helpers.py +++ b/tests/strategy/test_strategy_helpers.py @@ -5,29 +5,8 @@ import pytest from freqtrade.data.dataprovider import DataProvider from freqtrade.enums import CandleType from freqtrade.resolvers.strategy_resolver import StrategyResolver -from freqtrade.strategy import (merge_informative_pair, stoploss_from_absolute, stoploss_from_open, - timeframe_to_minutes) -from tests.conftest import get_patched_exchange - - -def generate_test_data(timeframe: str, size: int, start: str = '2020-07-05'): - np.random.seed(42) - tf_mins = timeframe_to_minutes(timeframe) - - base = np.random.normal(20, 2, size=size) - - date = pd.date_range(start, periods=size, freq=f'{tf_mins}min', tz='UTC') - df = pd.DataFrame({ - 'date': date, - 'open': base, - 'high': base + np.random.normal(2, 1, size=size), - 'low': base - np.random.normal(2, 1, size=size), - 'close': base + np.random.normal(0, 1, size=size), - 'volume': np.random.normal(200, size=size) - } - ) - df = df.dropna() - return df +from freqtrade.strategy import merge_informative_pair, stoploss_from_absolute, stoploss_from_open +from tests.conftest import generate_test_data, get_patched_exchange def test_merge_informative_pair(): From edb942f6624957c46ec4cf83618e2f2b2301ef6f Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 3 Oct 2022 20:47:26 +0200 Subject: [PATCH 107/126] Add typing import to sampleStrategy --- freqtrade/templates/sample_strategy.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/templates/sample_strategy.py b/freqtrade/templates/sample_strategy.py index 1eb3d4256..fd81570fe 100644 --- a/freqtrade/templates/sample_strategy.py +++ b/freqtrade/templates/sample_strategy.py @@ -5,6 +5,7 @@ import numpy as np # noqa import pandas as pd # noqa from pandas import DataFrame +from typing import Optional, Union from freqtrade.strategy import (BooleanParameter, CategoricalParameter, DecimalParameter, IStrategy, IntParameter) From 02e238a944d7a87a207d6f552e97f6e2b3771eac Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 3 Oct 2022 20:49:54 +0200 Subject: [PATCH 108/126] Combine ohlcv data in exchange class for live mode --- freqtrade/exchange/exchange.py | 49 +++++++++++++++++++++++----------- 1 file changed, 34 insertions(+), 15 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index cb9cbebbd..4f869f994 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -18,12 +18,12 @@ import ccxt.async_support as ccxt_async from cachetools import TTLCache from ccxt import ROUND_DOWN, ROUND_UP, TICK_SIZE, TRUNCATE, decimal_to_precision from dateutil import parser -from pandas import DataFrame +from pandas import DataFrame, concat from freqtrade.constants import (DEFAULT_AMOUNT_RESERVE_PERCENT, NON_OPEN_EXCHANGE_STATES, BuySell, Config, EntryExit, ListPairsWithTimeframes, MakerTaker, PairWithTimeframe) -from freqtrade.data.converter import ohlcv_to_dataframe, trades_dict_to_list +from freqtrade.data.converter import clean_ohlcv_dataframe, ohlcv_to_dataframe, trades_dict_to_list from freqtrade.enums import OPTIMIZE_MODES, CandleType, MarginMode, TradingMode from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFundsError, InvalidOrderException, OperationalException, PricingError, @@ -1850,10 +1850,14 @@ class Exchange: return pair, timeframe, candle_type, data def _build_coroutine(self, pair: str, timeframe: str, candle_type: CandleType, - since_ms: Optional[int]) -> Coroutine: + since_ms: Optional[int], cache: bool) -> Coroutine: + not_all_data = self.required_candle_call_count > 1 + if cache and (pair, timeframe, candle_type) in self._klines: + # Not in cache - force multi-calls + not_all_data = False if (not since_ms - and (self._ft_has["ohlcv_require_since"] or self.required_candle_call_count > 1)): + and (self._ft_has["ohlcv_require_since"] or not_all_data)): # Multiple calls for one pair - to get more history one_call = timeframe_to_msecs(timeframe) * self.ohlcv_candle_limit( timeframe, candle_type, since_ms) @@ -1890,8 +1894,9 @@ class Exchange: if ((pair, timeframe, candle_type) not in self._klines or not cache or self._now_is_time_to_refresh(pair, timeframe, candle_type)): - input_coroutines.append(self._build_coroutine( - pair, timeframe, candle_type=candle_type, since_ms=since_ms)) + + input_coroutines.append( + self._build_coroutine(pair, timeframe, candle_type, since_ms, cache)) else: logger.debug( @@ -1901,6 +1906,25 @@ class Exchange: return input_coroutines, cached_pairs + def _process_ohlcv_df(self, pair: str, timeframe: str, c_type: CandleType, ticks: List[List], + cache: bool, drop_incomplete: bool) -> DataFrame: + # keeping last candle time as last refreshed time of the pair + if ticks: + self._pairs_last_refresh_time[(pair, timeframe, c_type)] = ticks[-1][0] // 1000 + # keeping parsed dataframe in cache + ohlcv_df = ohlcv_to_dataframe(ticks, timeframe, pair=pair, fill_missing=True, + drop_incomplete=drop_incomplete) + if cache: + if (pair, timeframe, c_type) in self._klines: + old = self._klines[(pair, timeframe, c_type)] + # Reassign so we return the updated, combined df + ohlcv_df = clean_ohlcv_dataframe(concat([old, ohlcv_df], axis=0), timeframe, pair, + fill_missing=True, drop_incomplete=False) + self._klines[(pair, timeframe, c_type)] = ohlcv_df + else: + self._klines[(pair, timeframe, c_type)] = ohlcv_df + return ohlcv_df + def refresh_latest_ohlcv(self, pair_list: ListPairsWithTimeframes, *, since_ms: Optional[int] = None, cache: bool = True, drop_incomplete: Optional[bool] = None @@ -1937,16 +1961,11 @@ class Exchange: continue # Deconstruct tuple (has 4 elements) pair, timeframe, c_type, ticks = res - # keeping last candle time as last refreshed time of the pair - if ticks: - self._pairs_last_refresh_time[(pair, timeframe, c_type)] = ticks[-1][0] // 1000 - # keeping parsed dataframe in cache - ohlcv_df = ohlcv_to_dataframe( - ticks, timeframe, pair=pair, fill_missing=True, - drop_incomplete=drop_incomplete) + ohlcv_df = self._process_ohlcv_df( + pair, timeframe, c_type, ticks, cache, drop_incomplete) + results_df[(pair, timeframe, c_type)] = ohlcv_df - if cache: - self._klines[(pair, timeframe, c_type)] = ohlcv_df + # Return cached klines for pair, timeframe, c_type in cached_pairs: results_df[(pair, timeframe, c_type)] = self.klines( From b7f26e4f96c444f15025a83e5fcb11529b611f85 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 4 Oct 2022 20:23:20 +0200 Subject: [PATCH 109/126] Update some formatting issues --- freqtrade/exchange/exchange.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 4f869f994..5b7ab1f7f 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1882,10 +1882,8 @@ class Exchange: input_coroutines = [] cached_pairs = [] for pair, timeframe, candle_type in set(pair_list): - if ( - timeframe not in self.timeframes - and candle_type in (CandleType.SPOT, CandleType.FUTURES) - ): + if (timeframe not in self.timeframes + and candle_type in (CandleType.SPOT, CandleType.FUTURES)): logger.warning( f"Cannot download ({pair}, {timeframe}) combination as this timeframe is " f"not available on {self.name}. Available timeframes are " From cea017e79fbcd0d0a630d60d70a38dad844a2ab1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 4 Oct 2022 20:48:04 +0200 Subject: [PATCH 110/126] Age out old candles --- freqtrade/exchange/exchange.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 5b7ab1f7f..278b216ad 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -184,8 +184,9 @@ class Exchange: # Initial markets load self._load_markets() self.validate_config(config) + self._startup_candle_count: int = config.get('startup_candle_count', 0) self.required_candle_call_count = self.validate_required_startup_candles( - config.get('startup_candle_count', 0), config.get('timeframe', '')) + self._startup_candle_count, config.get('timeframe', '')) # Converts the interval provided in minutes in config to seconds self.markets_refresh_interval: int = exchange_config.get( @@ -1918,6 +1919,9 @@ class Exchange: # Reassign so we return the updated, combined df ohlcv_df = clean_ohlcv_dataframe(concat([old, ohlcv_df], axis=0), timeframe, pair, fill_missing=True, drop_incomplete=False) + candle_limit = self.ohlcv_candle_limit(timeframe, self._config['candle_type_def']) + # Age out old candles + ohlcv_df = ohlcv_df.tail(candle_limit + self._startup_candle_count) self._klines[(pair, timeframe, c_type)] = ohlcv_df else: self._klines[(pair, timeframe, c_type)] = ohlcv_df From 678272e2efbf86805286f4447fd7c6951815085b Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 5 Oct 2022 07:03:06 +0200 Subject: [PATCH 111/126] Improve test formatting --- tests/exchange/test_exchange.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 37ba2ca97..50ff4ae04 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -2083,7 +2083,7 @@ async def test__async_get_historic_ohlcv(default_conf, mocker, caplog, exchange_ def test_refresh_latest_ohlcv(mocker, default_conf, caplog, candle_type) -> None: ohlcv = [ [ - (arrow.utcnow().int_timestamp - 1) * 1000, # unix timestamp ms + (arrow.utcnow().shift(minutes=-5).int_timestamp) * 1000, # unix timestamp ms 1, # open 2, # high 3, # low @@ -2159,6 +2159,7 @@ def test_refresh_latest_ohlcv(mocker, default_conf, caplog, candle_type) -> None assert exchange._api_async.fetch_ohlcv.call_count == 3 exchange._api_async.fetch_ohlcv.reset_mock() caplog.clear() + # Call with invalid timeframe res = exchange.refresh_latest_ohlcv([('IOTA/ETH', '3m', candle_type)], cache=False) if candle_type != CandleType.MARK: From 638515bce5bcd244e253b3f8980fabb248786e18 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 5 Oct 2022 07:08:40 +0200 Subject: [PATCH 112/126] Test advanced caching --- tests/exchange/test_exchange.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 50ff4ae04..bf9c1ae72 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -2140,10 +2140,22 @@ def test_refresh_latest_ohlcv(mocker, default_conf, caplog, candle_type) -> None assert len(res) == len(pairs) assert exchange._api_async.fetch_ohlcv.call_count == 0 - exchange.required_candle_call_count = 1 assert log_has(f"Using cached candle (OHLCV) data for {pairs[0][0]}, " f"{pairs[0][1]}, {candle_type} ...", caplog) + caplog.clear() + # Reset refresh times - must do 1 call per pair (even though required_calls is 2) + exchange._pairs_last_refresh_time = {} + res = exchange.refresh_latest_ohlcv( + [('IOTA/ETH', '5m', candle_type), ('XRP/ETH', '5m', candle_type)]) + assert len(res) == len(pairs) + + assert exchange._api_async.fetch_ohlcv.call_count == 2 + + # cache - but disabled caching + exchange._api_async.fetch_ohlcv.reset_mock() + exchange.required_candle_call_count = 1 + pairlist = [ ('IOTA/ETH', '5m', candle_type), ('XRP/ETH', '5m', candle_type), From f475c6c3053b67b98da62450adead0cb16240420 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 5 Oct 2022 19:39:28 +0200 Subject: [PATCH 113/126] Add Specific, time-sensitive test-case for new behavior --- tests/conftest.py | 7 +++ tests/exchange/test_exchange.py | 76 ++++++++++++++++++++++++++++++++- 2 files changed, 82 insertions(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index f87fa59c5..9f71709f1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -104,6 +104,13 @@ def generate_test_data(timeframe: str, size: int, start: str = '2020-07-05'): return df +def generate_test_data_raw(timeframe: str, size: int, start: str = '2020-07-05'): + """ Generates data in the ohlcv format used by ccxt """ + df = generate_test_data(timeframe, size, start) + df['date'] = df.loc[:, 'date'].view(np.int64) // 1000 // 1000 + return list(list(x) for x in zip(*(df[x].values.tolist() for x in df.columns))) + + # Source: https://stackoverflow.com/questions/29881236/how-to-mock-asyncio-coroutines # TODO: This should be replaced with AsyncMock once support for python 3.7 is dropped. def get_mock_coro(return_value=None, side_effect=None): diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index bf9c1ae72..bcd1473c6 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -22,7 +22,8 @@ from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, API_RETRY_CO calculate_backoff, remove_credentials) from freqtrade.exchange.exchange import amount_to_contract_precision from freqtrade.resolvers.exchange_resolver import ExchangeResolver -from tests.conftest import get_mock_coro, get_patched_exchange, log_has, log_has_re, num_log_has_re +from tests.conftest import (generate_test_data_raw, get_mock_coro, get_patched_exchange, log_has, + log_has_re, num_log_has_re) # Make sure to always keep one exchange here which is NOT subclassed!! @@ -2182,6 +2183,79 @@ def test_refresh_latest_ohlcv(mocker, default_conf, caplog, candle_type) -> None assert len(res) == 1 +@pytest.mark.parametrize('candle_type', [CandleType.FUTURES, CandleType.MARK, CandleType.SPOT]) +def test_refresh_latest_ohlcv_cache(mocker, default_conf, candle_type, time_machine) -> None: + start = datetime(2021, 8, 1, 0, 0, 0, 0, tzinfo=timezone.utc) + ohlcv = generate_test_data_raw('1h', 100, start.strftime('%Y-%m-%d')) + time_machine.move_to(start + timedelta(hours=99, minutes=30)) + + exchange = get_patched_exchange(mocker, default_conf) + exchange._api_async.fetch_ohlcv = get_mock_coro(ohlcv) + pair1 = ('IOTA/ETH', '1h', candle_type) + pair2 = ('XRP/ETH', '1h', candle_type) + pairs = [pair1, pair2] + + # No caching + assert not exchange._klines + res = exchange.refresh_latest_ohlcv(pairs, cache=False) + assert exchange._api_async.fetch_ohlcv.call_count == 2 + assert len(res) == 2 + assert len(res[pair1]) == 99 + assert len(res[pair2]) == 99 + assert not exchange._klines + exchange._api_async.fetch_ohlcv.reset_mock() + + # With caching + res = exchange.refresh_latest_ohlcv(pairs) + assert exchange._api_async.fetch_ohlcv.call_count == 2 + assert len(res) == 2 + assert len(res[pair1]) == 99 + assert len(res[pair2]) == 99 + assert exchange._klines + assert exchange._pairs_last_refresh_time[pair1] == ohlcv[-1][0] // 1000 + exchange._api_async.fetch_ohlcv.reset_mock() + + # Returned from cache + res = exchange.refresh_latest_ohlcv(pairs) + assert exchange._api_async.fetch_ohlcv.call_count == 0 + assert len(res) == 2 + assert len(res[pair1]) == 99 + assert len(res[pair2]) == 99 + assert exchange._pairs_last_refresh_time[pair1] == ohlcv[-1][0] // 1000 + + # Move time 1 candle further but result didn't change yet + time_machine.move_to(start + timedelta(hours=101)) + res = exchange.refresh_latest_ohlcv(pairs) + assert exchange._api_async.fetch_ohlcv.call_count == 2 + assert len(res) == 2 + assert len(res[pair1]) == 99 + assert len(res[pair2]) == 99 + assert exchange._pairs_last_refresh_time[pair1] == ohlcv[-1][0] // 1000 + refresh_pior = exchange._pairs_last_refresh_time[pair1] + + # New candle on exchange - only return 50 candles (but one candle further) + new_startdate = (start + timedelta(hours=51)).strftime('%Y-%m-%d %H:%M') + ohlcv = generate_test_data_raw('1h', 50, new_startdate) + exchange._api_async.fetch_ohlcv = get_mock_coro(ohlcv) + res = exchange.refresh_latest_ohlcv(pairs) + assert exchange._api_async.fetch_ohlcv.call_count == 2 + assert len(res) == 2 + assert len(res[pair1]) == 100 + assert len(res[pair2]) == 100 + assert refresh_pior != exchange._pairs_last_refresh_time[pair1] + + assert exchange._pairs_last_refresh_time[pair1] == ohlcv[-1][0] // 1000 + assert exchange._pairs_last_refresh_time[pair2] == ohlcv[-1][0] // 1000 + exchange._api_async.fetch_ohlcv.reset_mock() + + # Retry same call - no action. + res = exchange.refresh_latest_ohlcv(pairs) + assert exchange._api_async.fetch_ohlcv.call_count == 0 + assert len(res) == 2 + assert len(res[pair1]) == 100 + assert len(res[pair2]) == 100 + + @pytest.mark.asyncio @pytest.mark.parametrize("exchange_name", EXCHANGES) async def test__async_get_candle_history(default_conf, mocker, caplog, exchange_name): From 92a1d58df8687bfeda5ed0f57a8736152430fce4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 6 Oct 2022 06:41:56 +0200 Subject: [PATCH 114/126] Evict cache if we didn't get new candles for X hours --- freqtrade/exchange/exchange.py | 8 ++++++-- tests/exchange/test_exchange.py | 4 ++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 278b216ad..914f67e60 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1854,8 +1854,12 @@ class Exchange: since_ms: Optional[int], cache: bool) -> Coroutine: not_all_data = self.required_candle_call_count > 1 if cache and (pair, timeframe, candle_type) in self._klines: - # Not in cache - force multi-calls - not_all_data = False + candle_limit = self.ohlcv_candle_limit(timeframe, candle_type) + min_date = date_minus_candles(timeframe, candle_limit - 5).timestamp() + # Check if 1 call can get us updated candles without hole in the data. + if min_date < self._pairs_last_refresh_time.get((pair, timeframe, candle_type), 0): + # Cache can be used - do one-off call. + not_all_data = False if (not since_ms and (self._ft_has["ohlcv_require_since"] or not_all_data)): diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index bcd1473c6..d8f5b832c 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -2145,13 +2145,13 @@ def test_refresh_latest_ohlcv(mocker, default_conf, caplog, candle_type) -> None f"{pairs[0][1]}, {candle_type} ...", caplog) caplog.clear() - # Reset refresh times - must do 1 call per pair (even though required_calls is 2) + # Reset refresh times - must do 2 call per pair as cache is expired exchange._pairs_last_refresh_time = {} res = exchange.refresh_latest_ohlcv( [('IOTA/ETH', '5m', candle_type), ('XRP/ETH', '5m', candle_type)]) assert len(res) == len(pairs) - assert exchange._api_async.fetch_ohlcv.call_count == 2 + assert exchange._api_async.fetch_ohlcv.call_count == 4 # cache - but disabled caching exchange._api_async.fetch_ohlcv.reset_mock() From 7c702dd1069bbbe9f7208c8085faa113acc667f3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 6 Oct 2022 14:43:45 +0000 Subject: [PATCH 115/126] Add cache eviction --- freqtrade/exchange/exchange.py | 5 +++++ tests/exchange/test_exchange.py | 12 ++++++++++++ 2 files changed, 17 insertions(+) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 914f67e60..64ea3a5ca 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1860,6 +1860,11 @@ class Exchange: if min_date < self._pairs_last_refresh_time.get((pair, timeframe, candle_type), 0): # Cache can be used - do one-off call. not_all_data = False + else: + # Time jump detected, evict cache + logger.info( + f"Time jump detected. Evicting cache for {pair}, {timeframe}, {candle_type}") + del self._klines[(pair, timeframe, candle_type)] if (not since_ms and (self._ft_has["ohlcv_require_since"] or not_all_data)): diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index d8f5b832c..673dc7594 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -2255,6 +2255,18 @@ def test_refresh_latest_ohlcv_cache(mocker, default_conf, candle_type, time_mach assert len(res[pair1]) == 100 assert len(res[pair2]) == 100 + # Move to distant future (so a 1 call would cause a hole in the data) + time_machine.move_to(start + timedelta(hours=2000)) + ohlcv = generate_test_data_raw('1h', 100, start + timedelta(hours=1900)) + exchange._api_async.fetch_ohlcv = get_mock_coro(ohlcv) + res = exchange.refresh_latest_ohlcv(pairs) + + assert exchange._api_async.fetch_ohlcv.call_count == 2 + assert len(res) == 2 + # Cache eviction - new data. + assert len(res[pair1]) == 99 + assert len(res[pair2]) == 99 + @pytest.mark.asyncio @pytest.mark.parametrize("exchange_name", EXCHANGES) From 6e179c7699163ce6fe01a0bbd5b982ee6c87c325 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 6 Oct 2022 14:56:38 +0000 Subject: [PATCH 116/126] Only store tick refresh time if we cache --- freqtrade/exchange/exchange.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 64ea3a5ca..e06f5c7c6 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1866,8 +1866,7 @@ class Exchange: f"Time jump detected. Evicting cache for {pair}, {timeframe}, {candle_type}") del self._klines[(pair, timeframe, candle_type)] - if (not since_ms - and (self._ft_has["ohlcv_require_since"] or not_all_data)): + if (not since_ms and (self._ft_has["ohlcv_require_since"] or not_all_data)): # Multiple calls for one pair - to get more history one_call = timeframe_to_msecs(timeframe) * self.ohlcv_candle_limit( timeframe, candle_type, since_ms) @@ -1917,7 +1916,7 @@ class Exchange: def _process_ohlcv_df(self, pair: str, timeframe: str, c_type: CandleType, ticks: List[List], cache: bool, drop_incomplete: bool) -> DataFrame: # keeping last candle time as last refreshed time of the pair - if ticks: + if ticks and cache: self._pairs_last_refresh_time[(pair, timeframe, c_type)] = ticks[-1][0] // 1000 # keeping parsed dataframe in cache ohlcv_df = ohlcv_to_dataframe(ticks, timeframe, pair=pair, fill_missing=True, From 3e08c6e5409d3e1b9c6f787415869e3e49289a00 Mon Sep 17 00:00:00 2001 From: Timothy Pogue Date: Thu, 6 Oct 2022 14:12:44 -0600 Subject: [PATCH 117/126] testing/debugging ws client script --- scripts/ws_client.py | 318 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 318 insertions(+) create mode 100644 scripts/ws_client.py diff --git a/scripts/ws_client.py b/scripts/ws_client.py new file mode 100644 index 000000000..63a980069 --- /dev/null +++ b/scripts/ws_client.py @@ -0,0 +1,318 @@ +#!/usr/bin/env python3 +""" +Simple command line client for Testing/debugging +a Freqtrade bot's message websocket + +Should not import anything from freqtrade, +so it can be used as a standalone script. +""" +import argparse +import time +import websockets +import socket +import asyncio +import logging +import rapidjson +import pandas +import orjson +import sys +from pathlib import Path +from dateutil.relativedelta import relativedelta + + +logger = logging.getLogger("WebSocketClient") + + +# --------------------------------------------------------------------------- + +def setup_logging(filename: str): + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler(filename), + logging.StreamHandler() + ] + ) + + +def parse_args(): + parser = argparse.ArgumentParser() + parser.add_argument( + '-c', + '--config', + help='Specify configuration file (default: %(default)s). ', + dest='config', + type=str, + metavar='PATH', + default='config.json' + ) + parser.add_argument( + '-l', + '--logfile', + help='The filename to log to.', + dest='logfile', + type=str, + default='ws_client.log' + ) + + args = parser.parse_args() + return vars(args) + + +def load_config(configfile): + file = Path(configfile) + if file.is_file(): + with file.open("r") as f: + config = rapidjson.load(f, parse_mode=rapidjson.PM_COMMENTS | + rapidjson.PM_TRAILING_COMMAS) + return config + else: + logger.warning(f"Could not load config file {file}.") + sys.exit(1) + + +def readable_timedelta(delta): + """ + Convert a dateutil.relativedelta to a readable format + + :param delta: A dateutil.relativedelta + :returns: The readable time difference string + """ + attrs = ['years', 'months', 'days', 'hours', 'minutes', 'seconds', 'microseconds'] + return ", ".join([ + '%d %s' % (getattr(delta, attr), attr if getattr(delta, attr) > 1 else attr[:-1]) + for attr in attrs if getattr(delta, attr) + ]) + +# ---------------------------------------------------------------------------- + + +def json_serialize(message): + """ + Serialize a message to JSON using orjson + :param message: The message to serialize + """ + return str(orjson.dumps(message), "utf-8") + + +def json_deserialize(message): + """ + Deserialize JSON to a dict + :param message: The message to deserialize + """ + def json_to_dataframe(data: str) -> pandas.DataFrame: + dataframe = pandas.read_json(data, orient='split') + if 'date' in dataframe.columns: + dataframe['date'] = pandas.to_datetime(dataframe['date'], unit='ms', utc=True) + + return dataframe + + def _json_object_hook(z): + if z.get('__type__') == 'dataframe': + return json_to_dataframe(z.get('__value__')) + return z + + return rapidjson.loads(message, object_hook=_json_object_hook) + + +# --------------------------------------------------------------------------- + + +class ClientProtocol: + logger = logging.getLogger("WebSocketClient.Protocol") + _MESSAGE_COUNT = 0 + _LAST_RECEIVED_AT = 0 # The epoch we received a message most recently + + async def on_connect(self, websocket): + # On connection we have to send our initial requests + initial_requests = [ + { + "type": "subscribe", # The subscribe request should always be first + "data": ["analyzed_df", "whitelist"] # The message types we want + }, + { + "type": "whitelist", + "data": None, + }, + { + "type": "analyzed_df", + "data": {"limit": 1500} + } + ] + + for request in initial_requests: + await websocket.send(json_serialize(request)) + + async def on_message(self, websocket, name, message): + deserialized = json_deserialize(message) + + message_size = sys.getsizeof(message) + message_type = deserialized.get('type') + message_data = deserialized.get('data') + + self.logger.info( + f"Received message of type {message_type} [{message_size} bytes] @ [{name}]" + ) + + time_difference = self._calculate_time_difference() + + if self._MESSAGE_COUNT > 0: + self.logger.info(f"Time since last message: {time_difference}") + + message_handler = getattr(self, f"_handle_{message_type}", None) or self._handle_default + await message_handler(name, message_type, message_data) + + self._MESSAGE_COUNT += 1 + self.logger.info(f"[{self._MESSAGE_COUNT}] total messages..") + self.logger.info("-" * 80) + + def _calculate_time_difference(self): + old_last_received_at = self._LAST_RECEIVED_AT + self._LAST_RECEIVED_AT = time.time() + time_delta = relativedelta(seconds=(self._LAST_RECEIVED_AT - old_last_received_at)) + + return readable_timedelta(time_delta) + + async def _handle_whitelist(self, name, type, data): + self.logger.info(data) + + async def _handle_default(self, name, type, data): + key, la, df = data['key'], data['la'], data['df'] + + columns = ", ".join([str(column) for column in df.columns]) + + self.logger.info(key) + self.logger.info(f"Last analyzed datetime: {la}") + self.logger.info(f"Latest candle datetime: {df.iloc[-1]['date']}") + self.logger.info(f"DataFrame length: {len(df)}") + self.logger.info(f"DataFrame columns: {columns}") + + +async def create_client( + host, + port, + token, + name='default', + protocol=ClientProtocol(), + sleep_time=10, + ping_timeout=10, + wait_timeout=30, + **kwargs +): + """ + Create a websocket client and listen for messages + :param host: The host + :param port: The port + :param token: The websocket auth token + :param name: The name of the producer + :param **kwargs: Any extra kwargs passed to websockets.connect + """ + + while 1: + try: + websocket_url = f"ws://{host}:{port}/api/v1/message/ws?token={token}" + logger.info(f"Attempting to connect to {name} @ {host}:{port}") + + async with websockets.connect(websocket_url, **kwargs) as ws: + logger.info("Connection successful...") + await protocol.on_connect(ws) + + # Now listen for messages + while 1: + try: + message = await asyncio.wait_for( + ws.recv(), + timeout=wait_timeout + ) + + await protocol.on_message(ws, name, message) + + except ( + asyncio.TimeoutError, + websockets.exceptions.ConnectionClosed + ): + # Try pinging + try: + pong = ws.ping() + await asyncio.wait_for( + pong, + timeout=ping_timeout + ) + logger.info("Connection still alive...") + + continue + + except asyncio.TimeoutError: + logger.error(f"Ping timed out, retrying in {sleep_time}s") + await asyncio.sleep(sleep_time) + + break + + except ( + socket.gaierror, + ConnectionRefusedError, + websockets.exceptions.InvalidStatusCode, + websockets.exceptions.InvalidMessage + ) as e: + logger.error(f"Connection Refused - {e} retrying in {sleep_time}s") + await asyncio.sleep(sleep_time) + + continue + + except ( + websockets.exceptions.ConnectionClosedError, + websockets.exceptions.ConnectionClosedOK + ): + # Just keep trying to connect again indefinitely + await asyncio.sleep(sleep_time) + + continue + + except Exception as e: + # An unforseen error has occurred, log and try reconnecting again + logger.error("Unexpected error has occurred:") + logger.exception(e) + + continue + + +# --------------------------------------------------------------------------- + + +async def _main(args): + setup_logging(args['logfile']) + config = load_config(args['config']) + + emc_config = config.get('external_message_consumer', {}) + + producers = emc_config.get('producers', []) + producer = producers[0] + + wait_timeout = emc_config.get('wait_timeout', 300) + ping_timeout = emc_config.get('ping_timeout', 10) + sleep_time = emc_config.get('sleep_time', 10) + message_size_limit = (emc_config.get('message_size_limit', 8) << 20) + + await create_client( + producer['host'], + producer['port'], + producer['ws_token'], + producer['name'], + sleep_time=sleep_time, + ping_timeout=ping_timeout, + wait_timeout=wait_timeout, + max_size=message_size_limit + ) + + +def main(): + args = parse_args() + try: + asyncio.run(_main(args)) + except KeyboardInterrupt: + logger.info("Exiting...") + + +if __name__ == "__main__": + main() From b92b98af29efbe73c2877f4761ab88685342225a Mon Sep 17 00:00:00 2001 From: Timothy Pogue Date: Thu, 6 Oct 2022 14:33:04 -0600 Subject: [PATCH 118/126] fix formatting --- scripts/ws_client.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/scripts/ws_client.py b/scripts/ws_client.py index 63a980069..51ba0ee83 100644 --- a/scripts/ws_client.py +++ b/scripts/ws_client.py @@ -7,16 +7,17 @@ Should not import anything from freqtrade, so it can be used as a standalone script. """ import argparse -import time -import websockets -import socket import asyncio import logging -import rapidjson -import pandas -import orjson +import socket import sys +import time from pathlib import Path + +import orjson +import pandas +import rapidjson +import websockets from dateutil.relativedelta import relativedelta From 1595e5fd8a69de366c8b4710e75e7099a91db027 Mon Sep 17 00:00:00 2001 From: Timothy Pogue Date: Thu, 6 Oct 2022 21:00:28 -0600 Subject: [PATCH 119/126] small fix in protocol --- scripts/ws_client.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/scripts/ws_client.py b/scripts/ws_client.py index 51ba0ee83..0cc076641 100644 --- a/scripts/ws_client.py +++ b/scripts/ws_client.py @@ -181,13 +181,14 @@ class ClientProtocol: async def _handle_default(self, name, type, data): key, la, df = data['key'], data['la'], data['df'] - columns = ", ".join([str(column) for column in df.columns]) + if not df.empty: + columns = ", ".join([str(column) for column in df.columns]) - self.logger.info(key) - self.logger.info(f"Last analyzed datetime: {la}") - self.logger.info(f"Latest candle datetime: {df.iloc[-1]['date']}") - self.logger.info(f"DataFrame length: {len(df)}") - self.logger.info(f"DataFrame columns: {columns}") + self.logger.info(key) + self.logger.info(f"Last analyzed datetime: {la}") + self.logger.info(f"Latest candle datetime: {df.iloc[-1]['date']}") + self.logger.info(f"DataFrame length: {len(df)}") + self.logger.info(f"DataFrame columns: {columns}") async def create_client( @@ -275,6 +276,7 @@ async def create_client( logger.error("Unexpected error has occurred:") logger.exception(e) + await asyncio.sleep(sleep_time) continue From fab6b2f1052ad479b95905f9b183fbff274858c4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 7 Oct 2022 14:00:04 +0200 Subject: [PATCH 120/126] Align datetime import in fiat_convert --- freqtrade/rpc/fiat_convert.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/rpc/fiat_convert.py b/freqtrade/rpc/fiat_convert.py index cbe4c0045..512e0947a 100644 --- a/freqtrade/rpc/fiat_convert.py +++ b/freqtrade/rpc/fiat_convert.py @@ -3,8 +3,8 @@ Module that define classes to convert Crypto-currency to FIAT e.g BTC to USD """ -import datetime import logging +from datetime import datetime from typing import Dict, List from cachetools import TTLCache @@ -67,7 +67,7 @@ class CryptoToFiatConverter(LoggingMixin): logger.warning( "Too many requests for CoinGecko API, backing off and trying again later.") # Set backoff timestamp to 60 seconds in the future - self._backoff = datetime.datetime.now().timestamp() + 60 + self._backoff = datetime.now().timestamp() + 60 return # If the request is not a 429 error we want to raise the normal error logger.error( @@ -81,7 +81,7 @@ class CryptoToFiatConverter(LoggingMixin): def _get_gekko_id(self, crypto_symbol): if not self._coinlistings: - if self._backoff <= datetime.datetime.now().timestamp(): + if self._backoff <= datetime.now().timestamp(): self._load_cryptomap() # Still not loaded. if not self._coinlistings: From a5bf34587a41ebbd4af9cc2fc2b2e92b408a20eb Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 7 Oct 2022 15:41:42 +0200 Subject: [PATCH 121/126] Improve fiat-convert behavior in case of coingecko outage --- freqtrade/rpc/fiat_convert.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/fiat_convert.py b/freqtrade/rpc/fiat_convert.py index 512e0947a..24c34af72 100644 --- a/freqtrade/rpc/fiat_convert.py +++ b/freqtrade/rpc/fiat_convert.py @@ -46,7 +46,9 @@ class CryptoToFiatConverter(LoggingMixin): if CryptoToFiatConverter.__instance is None: CryptoToFiatConverter.__instance = object.__new__(cls) try: - CryptoToFiatConverter._coingekko = CoinGeckoAPI() + # Limit retires to 1 (0 and 1) + # otherwise we risk bot impact if coingecko is down. + CryptoToFiatConverter._coingekko = CoinGeckoAPI(retries=1) except BaseException: CryptoToFiatConverter._coingekko = None return CryptoToFiatConverter.__instance From d42fb156086122c9031354740d7e491fc0d4f165 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 7 Oct 2022 16:05:41 +0200 Subject: [PATCH 122/126] Improve generic exception handler --- freqtrade/freqai/freqai_interface.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/freqai/freqai_interface.py b/freqtrade/freqai/freqai_interface.py index 5ac7bc32c..394b98e94 100644 --- a/freqtrade/freqai/freqai_interface.py +++ b/freqtrade/freqai/freqai_interface.py @@ -211,7 +211,8 @@ class IFreqaiModel(ABC): new_trained_timerange, pair, strategy, dk, data_load_timerange ) except Exception as msg: - logger.warning(f'Training {pair} raised exception {msg}, skipping.') + logger.warning(f"Training {pair} raised exception {msg.__class__.__name__}. " + f"Message: {msg}, skipping.") self.train_timer('stop') From 0460f362fb5b9160d065bdbadef00cb69dd73dfa Mon Sep 17 00:00:00 2001 From: Timothy Pogue Date: Fri, 7 Oct 2022 10:41:06 -0600 Subject: [PATCH 123/126] typo in handle func name --- scripts/ws_client.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/scripts/ws_client.py b/scripts/ws_client.py index 0cc076641..83182ae9e 100644 --- a/scripts/ws_client.py +++ b/scripts/ws_client.py @@ -170,15 +170,15 @@ class ClientProtocol: def _calculate_time_difference(self): old_last_received_at = self._LAST_RECEIVED_AT - self._LAST_RECEIVED_AT = time.time() - time_delta = relativedelta(seconds=(self._LAST_RECEIVED_AT - old_last_received_at)) + self._LAST_RECEIVED_AT = time.time() * 1000 + time_delta = relativedelta(microseconds=(self._LAST_RECEIVED_AT - old_last_received_at)) return readable_timedelta(time_delta) async def _handle_whitelist(self, name, type, data): self.logger.info(data) - async def _handle_default(self, name, type, data): + async def _handle_analyzed_df(self, name, type, data): key, la, df = data['key'], data['la'], data['df'] if not df.empty: @@ -189,6 +189,12 @@ class ClientProtocol: self.logger.info(f"Latest candle datetime: {df.iloc[-1]['date']}") self.logger.info(f"DataFrame length: {len(df)}") self.logger.info(f"DataFrame columns: {columns}") + else: + self.logger.info("Empty DataFrame") + + async def _handle_default(self, name, type, data): + self.logger.info("Unkown message of type {type} received...") + self.logger.info(data) async def create_client( From e337d4b78ae39d992e937b4fa06205b787a82871 Mon Sep 17 00:00:00 2001 From: Emre Date: Fri, 7 Oct 2022 19:05:49 +0300 Subject: [PATCH 124/126] Reset dataframe index after slice --- freqtrade/freqai/data_drawer.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqai/data_drawer.py b/freqtrade/freqai/data_drawer.py index cde72bfb5..31c76a68e 100644 --- a/freqtrade/freqai/data_drawer.py +++ b/freqtrade/freqai/data_drawer.py @@ -618,7 +618,8 @@ class FreqaiDataDrawer: ) for tf in self.freqai_info["feature_parameters"].get("include_timeframes"): - base_dataframes[tf] = dk.slice_dataframe(timerange, historic_data[pair][tf]) + base_dataframes[tf] = dk.slice_dataframe( + timerange, historic_data[pair][tf]).reset_index(drop=True) if pairs: for p in pairs: if pair in p: @@ -627,7 +628,7 @@ class FreqaiDataDrawer: corr_dataframes[p] = {} corr_dataframes[p][tf] = dk.slice_dataframe( timerange, historic_data[p][tf] - ) + ).reset_index(drop=True) return corr_dataframes, base_dataframes From ffebd812d1a69f42d102a821cd7ba36d187231e1 Mon Sep 17 00:00:00 2001 From: longyu Date: Sat, 8 Oct 2022 09:23:06 +0200 Subject: [PATCH 125/126] take less gpu memory to 20% --- user_data/freqaimodels/CatboostPredictionBinaryMultiModel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user_data/freqaimodels/CatboostPredictionBinaryMultiModel.py b/user_data/freqaimodels/CatboostPredictionBinaryMultiModel.py index 7882aca4a..cc39cec47 100644 --- a/user_data/freqaimodels/CatboostPredictionBinaryMultiModel.py +++ b/user_data/freqaimodels/CatboostPredictionBinaryMultiModel.py @@ -30,7 +30,7 @@ class CatboostPredictionBinaryMultiModel(BaseClassifierModel): cbr = CatBoostClassifier( allow_writing_files=False, - gpu_ram_part=0.5, + gpu_ram_part=0.2, verbose=100, early_stopping_rounds=400, **self.model_training_parameters, From a364b514234aa6e1d4acd243066003435ac7600a Mon Sep 17 00:00:00 2001 From: longyu Date: Fri, 14 Oct 2022 12:22:58 +0200 Subject: [PATCH 126/126] add strategy v4 --- .../FreqaiBinaryClassStrategy_v4.py | 553 ++++++++++++++++++ 1 file changed, 553 insertions(+) create mode 100644 user_data/strategies/FreqaiBinaryClassStrategy_v4.py diff --git a/user_data/strategies/FreqaiBinaryClassStrategy_v4.py b/user_data/strategies/FreqaiBinaryClassStrategy_v4.py new file mode 100644 index 000000000..61c5e5d78 --- /dev/null +++ b/user_data/strategies/FreqaiBinaryClassStrategy_v4.py @@ -0,0 +1,553 @@ +from typing import Dict, List, Optional, Tuple, Union +import logging +from functools import reduce +from turtle import update +from h11 import Data +from datetime import datetime, timedelta, timezone +import pandas as pd +import talib.abstract as ta +from pandas_ta.trend import adx +from pandas import DataFrame +from technical import qtpylib +import numpy as np +from scipy.signal import argrelextrema +from sklearn.metrics import precision_recall_curve +from freqtrade.exchange import timeframe_to_prev_date +from freqtrade.persistence import Trade +from technical.util import resample_to_interval, resampled_merge +from freqtrade.strategy import DecimalParameter, IntParameter, merge_informative_pair +from freqtrade.strategy.interface import IStrategy + + +logger = logging.getLogger(__name__) + + +def find_support_levels(df: DataFrame) -> DataFrame: + """ + cond1 = df['Low'][i] < df['Low'][i-1] + cond2 = df['Low'][i] < df['Low'][i+1] + cond3 = df['Low'][i+1] < df['Low'][i+2] + cond4 = df['Low'][i-1] < df['Low'][i-2] + """ + cond1 = df["low"] < df["low"].shift(1) + cond2 = df["low"] < df["low"].shift(-1) + cond3 = df["low"].shift(-1) < df["low"].shift(-2) + cond4 = df["low"].shift(1) < df["low"].shift(2) + return (cond1 & cond2 & cond3 & cond4) + + +def get_max_labels(df: DataFrame, alpha: float = 0.5) -> DataFrame: + + price = (df['high'] + df['low'] + df['close']) / 3 + + max_peaks = argrelextrema(price.values, np.greater, order=12)[0] + + out = adx(df["high"], df["low"], df["close"], window=12) + diplus = out["DMP_14"] + + di_thr = diplus[max_peaks].mean() + diplus[max_peaks].std() * alpha + + nn = 2 + labels = np.zeros(len(df), dtype=np.int32) + for mp in max_peaks: + ref_close = price.iloc[mp] + start = max(0, mp-nn) + end = min(df.shape[0], mp+nn+1) + pct = np.abs(price[start:end] / ref_close - 1) + is_close = np.where(pct <= 0.005)[0] + left_idx = is_close[0] + right_idx = is_close[-1] + # locality labeling + if diplus[mp-nn+left_idx:mp-nn+right_idx].mean() >= di_thr: + labels[mp-nn+left_idx:mp-nn+right_idx] = 1 + if labels.max() == 0: # if not any positive label is found, we force it + idx = np.nanargmax(diplus[max_peaks]) + labels[max_peaks[idx]] = 1 + return labels + + +def get_min_labels(df: DataFrame, alpha : float = 0.5) -> DataFrame: + + price = (df['high'] + df['low'] + df['close']) / 3 + + min_peaks = argrelextrema(price.values, np.less, order=12)[0] + + out = adx(df["high"], df["low"], df["close"], window=12) + diminus = out["DMN_14"] + di_thr = diminus[min_peaks].mean() + diminus[min_peaks].std() * alpha + nn = 2 + labels = np.zeros(len(df), dtype=np.int32) + for mp in min_peaks: + ref_close = price.iloc[mp] + start = max(0, mp-nn) + end = min(df.shape[0], mp+nn+1) + pct = np.abs(price[start:end] / ref_close - 1) + is_close = np.where(pct <= 0.005)[0] + left_idx = is_close[0] + right_idx = is_close[-1] + # locality labeling + if diminus[mp-nn+left_idx:mp-nn+right_idx].mean() >= di_thr: + labels[mp-nn+left_idx:mp-nn+right_idx] = 1 + # return np.array([str(x) for x in labels]).astype(np.object0) + if labels.max() == 0: # if not any positive label is found, we force it + idx = np.nanargmax(diminus[min_peaks]) + labels[min_peaks[idx]] = 1 + return labels + + +def expand_labels(df: DataFrame, peaks: List[int]): + nn = 2 + labels = np.zeros(len(df), dtype=np.int32) + price = (df['high'] + df['low'] + df['close']) / 3 + for p in peaks: + ref_price = price[p] + start = max(0, p - nn) + end = min(df.shape[0], p + nn + 1) + pct = np.abs(price[start:end] / ref_price - 1) + is_close = np.where(pct <= 0.005)[0] + left_idx = is_close[0] + right_idx = is_close[-1] + # locality labeling + labels[p-nn+left_idx:p-nn+right_idx] = 1 + return labels + + +def find_labels(df: DataFrame, alpha=0.1) -> DataFrame: + """Find min/max locals.""" + max_peaks = get_max_labels(df, alpha=alpha).nonzero()[0] + min_peaks = get_min_labels(df, alpha=alpha).nonzero()[0] + price = (df['high'] + df['low'] + df['close']) / 3 + peaks = sorted(set(min_peaks).union(set(max_peaks))) + updown = None + max_peaks2 = [] + min_peaks2 = [] + for idx in peaks: + if (idx in min_peaks and idx in max_peaks): + # one peak cant be at both sides. + continue + if idx in min_peaks: + if updown is None or updown == True: + updown = False + min_peaks2.append(idx) + else: + if price[min_peaks2[-1]] < price[idx]: + continue + else: + min_peaks2[-1] = idx + + elif idx in max_peaks: + if updown is None or updown == False: + updown = True + max_peaks2.append(idx) + else: + if price[max_peaks2[-1]] > price[idx]: + continue + else: + max_peaks2[-1] = idx + min_peaks = expand_labels(df, min_peaks2) + max_peaks = expand_labels(df, max_peaks2) + return min_peaks, max_peaks + + +class FreqaiBinaryClassStrategy_v4(IStrategy): + """ + Example strategy showing how the user connects their own + IFreqaiModel to the strategy. Namely, the user uses: + self.model = CustomModel(self.config) + self.model.bridge.start(dataframe, metadata) + + to make predictions on their data. populate_any_indicators() automatically + generates the variety of features indicated by the user in the + canonical freqtrade configuration file under config['freqai']. + """ + + minimal_roi = {"0": 0.1, "240": -1} + + plot_config = { + "main_plot": {}, + "subplots": { + "do_predict": { + "do_predict": { + "color": "brown" + } + }, + "DI_values": { + "DI_values": { + "color": "#8115a9", + "type": "line" + } + }, + "GTs": { + "tp_max": { + "color": "#69796a", + "type": "bar" + }, + "tp_min": { + "color": "#e2517f", + "type": "bar" + }, + "max": { + "color": "#69796a", + "type": "line" + }, + "min": { + "color": "#e2517f", + "type": "line" + }, + "neutral": { + "color": "#ffffff", + "type": "line" + } + } + } + } + + position_adjustment_enable = False + + process_only_new_candles = True + stoploss = -0.05 + use_exit_signal = True + startup_candle_count: int = 300 + can_short = True + + 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"] + informative_pairs = [] + for tf in self.config["freqai"]["feature_parameters"]["include_timeframes"]: + for pair in whitelist_pairs: + informative_pairs.append((pair, tf)) + for pair in corr_pairs: + if pair in whitelist_pairs: + continue # avoid duplication + informative_pairs.append((pair, tf)) + return informative_pairs + + def populate_any_indicators( + self, pair, df, tf, informative=None, set_generalized_indicators=False + ): + """ + Function designed to automatically generate, name and merge features + from user indicated timeframes in the configuration file. User controls the indicators + passed to the training/prediction by prepending indicators with `'%-' + coin ` + (see convention below). I.e. user should not prepend any supporting metrics + (e.g. bb_lowerband below) with % unless they explicitly want to pass that metric to the + model. + :params: + :pair: pair to be used as informative + :df: strategy dataframe which will receive merges from informatives + :tf: timeframe of the dataframe which will modify the feature names + :informative: the dataframe associated with the informative pair + :coin: the name of the coin which will modify the feature names. + """ + + coin = pair.split('/')[0] + + if informative is None: + informative = self.dp.get_pair_dataframe(pair, tf) + + # first loop is automatically duplicating indicators for time periods + for t in self.freqai_info["feature_parameters"]["indicator_periods_candles"]: + + 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) + out = adx(informative["high"], informative["low"], informative["close"], window=t) + informative[f"%-{coin}adx-period_{t}"] = out["ADX_14"] + informative[f"%-{coin}diplus-period_{t}"] = out["DMP_14"] + informative[f"%-{coin}diminus-period_{t}"] = out["DMN_14"] + + informative[f"{coin}20sma-period_{t}"] = ta.SMA(informative, timeperiod=t) + #informative[f"{coin}21ema-period_{t}"] = ta.EMA(informative, timeperiod=t) + informative[f"%-{coin}close_over_20sma-period_{t}"] = ( + informative["close"] / informative[f"{coin}20sma-period_{t}"] + ) + + bollinger = qtpylib.bollinger_bands( + qtpylib.typical_price(informative), window=t, stds=2.2 + ) + informative[f"{coin}bb_lowerband-period_{t}"] = bollinger["lower"] + informative[f"{coin}bb_middleband-period_{t}"] = bollinger["mid"] + informative[f"{coin}bb_upperband-period_{t}"] = bollinger["upper"] + + informative[f"%-{coin}bb_width-period_{t}"] = ( + informative[f"{coin}bb_upperband-period_{t}"] + - informative[f"{coin}bb_lowerband-period_{t}"] + ) / informative[f"{coin}bb_middleband-period_{t}"] + informative[f"%-{coin}close-bb_lower-period_{t}"] = ( + informative["close"] / informative[f"{coin}bb_lowerband-period_{t}"] + ) + + informative[f"%-{coin}roc-period_{t}"] = ta.ROC(informative, timeperiod=t) + macd = ta.MACD(informative, timeperiod=t) + informative[f"%-{coin}macd-period_{t}"] = macd["macd"] + + informative[f"%-{coin}relative_volume-period_{t}"] = ( + informative["volume"] / informative["volume"].rolling(t).mean() + ) + + informative[f"%-{coin}pct-change"] = informative["close"].pct_change() + informative[f"%-{coin}raw_volume"] = informative["volume"] + informative[f"%-{coin}raw_price"] = informative["close"] + + indicators = [col for col in informative if col.startswith("%")] + # This loop duplicates and shifts all indicators to add a sense of recency to data + for n in range(self.freqai_info["feature_parameters"]["include_shifted_candles"] + 1): + if n == 0: + continue + informative_shift = informative[indicators].shift(n) + informative_shift = informative_shift.add_suffix("_shift-" + str(n)) + informative = pd.concat((informative, informative_shift), axis=1) + + # find support levels + if tf == self.freqai_info["feature_parameters"]["include_timeframes"][-1]: + informative_6h = resample_to_interval(informative, "6h") + informative_6h["support_levels"] = find_support_levels(informative_6h) + df = merge_informative_pair(df, informative_6h, self.config["timeframe"], "6h", ffill=True) + + df = merge_informative_pair(df, informative, self.config["timeframe"], tf, ffill=True) + + skip_columns = [ + (s + "_" + tf) for s in ["date", "open", "high", "low", "close", "volume"] + ] + df = df.drop(columns=skip_columns) + + # Add generalized indicators here (because in live, it will call this + # function to populate indicators during training). Notice how we ensure not to + # add them multiple times + if set_generalized_indicators: + df["%-day_of_week"] = (df["date"].dt.dayofweek + 1) / 7 + df["%-hour_of_day"] = (df["date"].dt.hour + 1) / 25 + + # user adds targets here by prepending them with &- (see convention below) + # If user wishes to use multiple targets, a multioutput prediction model + # needs to be used such as templates/CatboostPredictionMultiModel.py + #df["&s-minima"] = FreqaiBinaryClassStrategy.get_min_labels(df) + #df["&s-maxima"] = FreqaiBinaryClassStrategy.get_max_labels(df) + minmax = np.array(["neutral"] * len(df)) + min_labels, max_labels = find_labels(df, alpha=-0.5) + minmax[min_labels == 1] = "min" + minmax[max_labels == 1] = "max" + df["&s-minmax"] = np.array([str(x) for x in minmax]).astype(np.object0) + return df + + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + + self.freqai_info = self.config["freqai"] + + # the model will return 4 values, its prediction, an indication of whether or not the + # prediction should be accepted, the target mean/std values from the labels used during + # each training period. + dataframe = self.freqai.start(dataframe, metadata, self) + # dataframe["&s-minima"] = dataframe["&s-minima"].astype(np.float32) + # dataframe["&s-maxima"] = dataframe["&s-maxima"].astype(np.float32) + min_labels, max_labels = find_labels(dataframe, alpha=-0.5) + + self.maxima_threhsold = 0.7 # dataframe["max"][dataframe["&s-minmax"] == "max"].mean() + self.minima_threhsold = 0.7 # dataframe["min"][dataframe["&s-minmax"] == "min"].mean() + + dataframe["tp_max"] = max_labels.astype(np.float32) + dataframe["tp_min"] = min_labels.astype(np.float32) + dataframe["di-"] = ta.MINUS_DI(dataframe, window=12) + dataframe["di+"] = ta.PLUS_DI(dataframe, window=12) + return dataframe + + def populate_entry_trend(self, df: DataFrame, metadata: dict) -> DataFrame: + hours_candle_stability = 4 + if df["do_predict"].rolling(12 * 4).sum().iloc[-1] == 12 * 4: # enter the market if last `hours_candle_stability` are stable + enter_long_conditions = [df["do_predict"] == 1, df["min"] >= self.minima_threhsold] + + if enter_long_conditions: + df.loc[ + reduce(lambda x, y: x & y, enter_long_conditions), ["enter_long", "enter_tag"] + ] = (1, "long") + + if self.can_short: + enter_short_conditions = [df["do_predict"] == 1, df["max"] >= self.maxima_threhsold] + + if enter_short_conditions: + df.loc[ + reduce(lambda x, y: x & y, enter_short_conditions), ["enter_short", "enter_tag"] + ] = (1, "short") + else: + df["enter_long", "enter_tag"] = (0, "long") + if self.can_short: + df["enter_short", "enter_tag"] = (0, "short") + return df + + def populate_exit_trend(self, df: DataFrame, metadata: dict) -> DataFrame: + exit_long_conditions = [df["do_predict"] == 1, df["max"] >= self.maxima_threhsold] + if exit_long_conditions: + df.loc[reduce(lambda x, y: x & y, exit_long_conditions), + ["exit_long", "exit_tag"]] = (1, "exit signal") + + if self.can_short: + exit_short_conditions = [df["do_predict"] == 1, df["min"] >= self.minima_threhsold] + if exit_short_conditions: + df.loc[reduce(lambda x, y: x & y, exit_short_conditions), "exit_short"] = 1 + + if self.config['runmode'].value in ('live', 'dry_run'): + trades = Trade.get_trades_proxy(pair=metadata["pair"], is_open=True) + if trades: + if df["do_predict"].iloc[-1] != 1: + avg_entry_price = sum([trade.open_rate * trade.amount for trade in trades]) / sum([trade.amount for trade in trades]) + if not trades[0].is_short: + profit = df["close"].iloc[-1] / avg_entry_price - 1 + else: + profit = avg_entry_price / df["close"].iloc[-1] - 1 + logger.warning(f"Market changed, {metadata['pair']} profit is {profit}") + # if profit < 0: # force sell + last_candle = np.zeros(df.shape[0]) + last_candle[-1] = 1 + cond = [df["do_predict"] != 1, last_candle] + df.loc[reduce(lambda x, y : x & y, cond), + [f"exit_{'short' if trades[0].is_short else 'long'}", "exit_tag"]] = (1, "OOD Exit") + return df + + 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.model.bridge.dd.pair_dict + else: + pair_dict = self.model.bridge.dd.follower_dict + + entry_tag = trade.enter_tag + + if ( + "prediction" + entry_tag not in pair_dict[pair] + or pair_dict[pair]["prediction" + entry_tag] > 0 + ): + with self.model.bridge.lock: + if entry_tag == "long": + pair_dict[pair]["prediction" + entry_tag] = abs(trade_candle["&s-maxima"]) + else: + pair_dict[pair]["prediction" + entry_tag] = abs(trade_candle["&-s_close"]) + if not follow_mode: + self.model.bridge.dd.save_drawer_to_disk() + else: + self.model.bridge.dd.save_follower_dict_to_disk() + + roi_price = pair_dict[pair]["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]["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, + order_type: str, + amount: float, + rate: float, + time_in_force: str, + current_time, + entry_tag, + side: str, + **kwargs, + ) -> bool: + + df, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) + last_candle = df.iloc[-1].squeeze() + + if side == "long": + if rate > (last_candle["close"] * (1 + 0.0025)): + return False + else: + if rate < (last_candle["close"] * (1 - 0.0025)): + return False + + return True + + def adjust_trade_position(self, trade: Trade, current_time: datetime, + current_rate: float, current_profit: float, + min_stake: Optional[float], max_stake: float, + **kwargs) -> Optional[float]: + """ + Custom trade adjustment logic, returning the stake amount that a trade should be increased. + This means extra buy orders with additional fees. + Only called when `position_adjustment_enable` is set to True. + + For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/ + + When not implemented by a strategy, returns None + + :param trade: trade object. + :param current_time: datetime object, containing the current datetime + :param current_rate: Current buy rate. + :param current_profit: Current profit (as ratio), calculated based on current_rate. + :param min_stake: Minimal stake size allowed by exchange. + :param max_stake: Balance available for trading. + :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. + :return float: Stake amount to adjust your trade + """ + if not trade.is_short: + if current_profit < -0.02: + df, _ = self.dp.get_analyzed_dataframe(trade.pair, self.timeframe) + try: + new_local_minima = [df["&s-minima"] > self.minima_threhsold, + (df["close"] / current_rate - 1) < 1e-3] + if df.shape[0] - df.loc[reduce(lambda x, y: x & y, new_local_minima)].index[-1] <= 10: + return 20 + except: + pass + return None