From 5b826150dff2c2d0f04b4a93ab802a5ce8b50c63 Mon Sep 17 00:00:00 2001 From: Wagner Costa Santos Date: Mon, 5 Sep 2022 17:43:28 -0300 Subject: [PATCH 01/33] fix hyperopt - freqai --- freqtrade/freqai/data_drawer.py | 29 +++++++++++++++++++++++----- freqtrade/freqai/freqai_interface.py | 7 +++++++ 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/freqtrade/freqai/data_drawer.py b/freqtrade/freqai/data_drawer.py index 9eeabef8f..a1ecb7654 100644 --- a/freqtrade/freqai/data_drawer.py +++ b/freqtrade/freqai/data_drawer.py @@ -93,6 +93,16 @@ class FreqaiDataDrawer: "model_filename": "", "trained_timestamp": 0, "priority": 1, "first": True, "data_path": "", "extras": {}} + def __getstate__(self): + """ + Return state values to be pickled. + It's necessary to allow serialization in hyperopt + """ + return ({ + "pair_dict": self.pair_dict, + "pair_dictionary_path": self.pair_dictionary_path + }) + def load_drawer_from_disk(self): """ Locate and load a previously saved data drawer full of all pair model metadata in @@ -155,14 +165,23 @@ class FreqaiDataDrawer: # create a backup shutil.copy(self.historic_predictions_path, self.historic_predictions_bkp_path) - def save_drawer_to_disk(self): + def save_drawer_to_disk(self, live=False): """ Save data drawer full of all pair model metadata in present model folder. """ - with self.save_lock: + if live: + with self.save_lock: + with open(self.pair_dictionary_path, 'w') as fp: + rapidjson.dump( + self.pair_dict, fp, default=self.np_encoder, + number_mode=rapidjson.NM_NATIVE + ) + else: + # save_lock it's not working with hyperopt with open(self.pair_dictionary_path, 'w') as fp: - rapidjson.dump(self.pair_dict, fp, default=self.np_encoder, - number_mode=rapidjson.NM_NATIVE) + rapidjson.dump( + self.pair_dict, fp, default=self.np_encoder, + number_mode=rapidjson.NM_NATIVE) def save_follower_dict_to_disk(self): """ @@ -437,7 +456,7 @@ class FreqaiDataDrawer: self.model_dictionary[coin] = model self.pair_dict[coin]["model_filename"] = dk.model_filename self.pair_dict[coin]["data_path"] = str(dk.data_path) - self.save_drawer_to_disk() + self.save_drawer_to_disk(dk.live) return diff --git a/freqtrade/freqai/freqai_interface.py b/freqtrade/freqai/freqai_interface.py index a9c21fb65..9a05e8383 100644 --- a/freqtrade/freqai/freqai_interface.py +++ b/freqtrade/freqai/freqai_interface.py @@ -90,6 +90,13 @@ class IFreqaiModel(ABC): self._threads: List[threading.Thread] = [] self._stop_event = threading.Event() + def __getstate__(self): + """ + Return state values to be pickled. + It's necessary to allow serialization in hyperopt + """ + return ({"dd": self.dd}) + def assert_config(self, config: Dict[str, Any]) -> None: if not config.get("freqai", {}): From 2c8e5b191bf84580684bf3a372121cef2e13d8f8 Mon Sep 17 00:00:00 2001 From: Wagner Costa Santos Date: Mon, 5 Sep 2022 17:43:55 -0300 Subject: [PATCH 02/33] fix hyperopt - freqai --- freqtrade/freqai/data_drawer.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/freqtrade/freqai/data_drawer.py b/freqtrade/freqai/data_drawer.py index a1ecb7654..dff6b5942 100644 --- a/freqtrade/freqai/data_drawer.py +++ b/freqtrade/freqai/data_drawer.py @@ -174,8 +174,7 @@ class FreqaiDataDrawer: with open(self.pair_dictionary_path, 'w') as fp: rapidjson.dump( self.pair_dict, fp, default=self.np_encoder, - number_mode=rapidjson.NM_NATIVE - ) + number_mode=rapidjson.NM_NATIVE) else: # save_lock it's not working with hyperopt with open(self.pair_dictionary_path, 'w') as fp: From 97077ba18acd9ea0ad67ba45e917aca6bdcb3b0d Mon Sep 17 00:00:00 2001 From: robcaulk Date: Tue, 6 Sep 2022 20:30:37 +0200 Subject: [PATCH 03/33] add continual learning to catboost and friends --- docs/freqai.md | 1 + freqtrade/freqai/freqai_interface.py | 3 ++- .../prediction_models/BaseClassifierModel.py | 2 +- .../prediction_models/BaseRegressionModel.py | 2 +- .../prediction_models/BaseTensorFlowModel.py | 2 +- .../prediction_models/CatboostClassifier.py | 11 ++++++++--- .../freqai/prediction_models/CatboostRegressor.py | 15 ++++++++------- .../CatboostRegressorMultiTarget.py | 7 +++++-- .../prediction_models/LightGBMClassifier.py | 11 ++++++++--- .../freqai/prediction_models/LightGBMRegressor.py | 11 ++++++++--- .../LightGBMRegressorMultiTarget.py | 7 +++++-- 11 files changed, 48 insertions(+), 24 deletions(-) diff --git a/docs/freqai.md b/docs/freqai.md index c0844bf32..e790bbb81 100644 --- a/docs/freqai.md +++ b/docs/freqai.md @@ -98,6 +98,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `expiration_hours` | Avoid making predictions if a model is more than `expiration_hours` old.
Defaults set to 0, which means models never expire.
**Datatype:** Positive integer. | `fit_live_predictions_candles` | Number of historical candles to use for computing target (label) statistics from prediction data, instead of from the training data set.
**Datatype:** Positive integer. | `follow_mode` | If true, this instance of FreqAI will look for models associated with `identifier` and load those for inferencing. A `follower` will **not** train new models.
**Datatype:** Boolean. Default: `False`. +| `continual_learning` | If true, FreqAI will start training new models from the final state of the most recently trained model.
**Datatype:** Boolean. Default: `False`. | | **Feature parameters** | `feature_parameters` | A dictionary containing the parameters used to engineer the feature set. Details and examples are shown [here](#feature-engineering).
**Datatype:** Dictionary. | `include_timeframes` | A list of timeframes that all indicators in `populate_any_indicators` will be created for. The list is added as features to the base asset feature set.
**Datatype:** List of timeframes (strings). diff --git a/freqtrade/freqai/freqai_interface.py b/freqtrade/freqai/freqai_interface.py index a9c21fb65..b6f3d8ebc 100644 --- a/freqtrade/freqai/freqai_interface.py +++ b/freqtrade/freqai/freqai_interface.py @@ -86,6 +86,7 @@ class IFreqaiModel(ABC): self.begin_time: float = 0 self.begin_time_train: float = 0 self.base_tf_seconds = timeframe_to_seconds(self.config['timeframe']) + self.continual_learning = self.freqai_info.get('continual_learning', False) self._threads: List[threading.Thread] = [] self._stop_event = threading.Event() @@ -674,7 +675,7 @@ class IFreqaiModel(ABC): """ @abstractmethod - def fit(self, data_dictionary: Dict[str, Any]) -> Any: + def fit(self, data_dictionary: Dict[str, Any], dk: FreqaiDataKitchen) -> Any: """ Most regressors use the same function names and arguments e.g. user can drop in LGBMRegressor in place of CatBoostRegressor and all data diff --git a/freqtrade/freqai/prediction_models/BaseClassifierModel.py b/freqtrade/freqai/prediction_models/BaseClassifierModel.py index 2edbf3b51..e51e26e0f 100644 --- a/freqtrade/freqai/prediction_models/BaseClassifierModel.py +++ b/freqtrade/freqai/prediction_models/BaseClassifierModel.py @@ -61,7 +61,7 @@ class BaseClassifierModel(IFreqaiModel): ) logger.info(f'Training model on {len(data_dictionary["train_features"])} data points') - model = self.fit(data_dictionary) + model = self.fit(data_dictionary, dk) logger.info(f"--------------------done training {pair}--------------------") diff --git a/freqtrade/freqai/prediction_models/BaseRegressionModel.py b/freqtrade/freqai/prediction_models/BaseRegressionModel.py index 2ef175a2e..45f0c2937 100644 --- a/freqtrade/freqai/prediction_models/BaseRegressionModel.py +++ b/freqtrade/freqai/prediction_models/BaseRegressionModel.py @@ -60,7 +60,7 @@ class BaseRegressionModel(IFreqaiModel): ) logger.info(f'Training model on {len(data_dictionary["train_features"])} data points') - model = self.fit(data_dictionary) + model = self.fit(data_dictionary, dk) logger.info(f"--------------------done training {pair}--------------------") diff --git a/freqtrade/freqai/prediction_models/BaseTensorFlowModel.py b/freqtrade/freqai/prediction_models/BaseTensorFlowModel.py index 04eff045f..66e6ec1fc 100644 --- a/freqtrade/freqai/prediction_models/BaseTensorFlowModel.py +++ b/freqtrade/freqai/prediction_models/BaseTensorFlowModel.py @@ -57,7 +57,7 @@ class BaseTensorFlowModel(IFreqaiModel): ) logger.info(f'Training model on {len(data_dictionary["train_features"])} data points') - model = self.fit(data_dictionary) + model = self.fit(data_dictionary, dk) logger.info(f"--------------------done training {pair}--------------------") diff --git a/freqtrade/freqai/prediction_models/CatboostClassifier.py b/freqtrade/freqai/prediction_models/CatboostClassifier.py index b88b28b25..13395879a 100644 --- a/freqtrade/freqai/prediction_models/CatboostClassifier.py +++ b/freqtrade/freqai/prediction_models/CatboostClassifier.py @@ -2,7 +2,7 @@ import logging from typing import Any, Dict from catboost import CatBoostClassifier, Pool - +from freqtrade.freqai.data_kitchen import FreqaiDataKitchen from freqtrade.freqai.prediction_models.BaseClassifierModel import BaseClassifierModel @@ -16,7 +16,7 @@ class CatboostClassifier(BaseClassifierModel): has its own DataHandler where data is held, saved, loaded, and managed. """ - def fit(self, data_dictionary: Dict) -> Any: + def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen) -> Any: """ User sets up the training and test data to fit their desired model here :params: @@ -36,6 +36,11 @@ class CatboostClassifier(BaseClassifierModel): **self.model_training_parameters, ) - cbr.fit(train_data) + if dk.pair not in self.dd.model_dictionary or not self.continual_learning: + init_model = None + else: + init_model = self.dd.model_dictionary[dk.pair] + + cbr.fit(train_data, init_model=init_model) return cbr diff --git a/freqtrade/freqai/prediction_models/CatboostRegressor.py b/freqtrade/freqai/prediction_models/CatboostRegressor.py index d93569c91..0b8bc162b 100644 --- a/freqtrade/freqai/prediction_models/CatboostRegressor.py +++ b/freqtrade/freqai/prediction_models/CatboostRegressor.py @@ -3,6 +3,7 @@ import logging from typing import Any, Dict from catboost import CatBoostRegressor, Pool +from freqtrade.freqai.data_kitchen import FreqaiDataKitchen from freqtrade.freqai.prediction_models.BaseRegressionModel import BaseRegressionModel @@ -17,7 +18,7 @@ class CatboostRegressor(BaseRegressionModel): has its own DataHandler where data is held, saved, loaded, and managed. """ - def fit(self, data_dictionary: Dict) -> Any: + def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen) -> Any: """ User sets up the training and test data to fit their desired model here :param data_dictionary: the dictionary constructed by DataHandler to hold @@ -38,16 +39,16 @@ class CatboostRegressor(BaseRegressionModel): weight=data_dictionary["test_weights"], ) + if dk.pair not in self.dd.model_dictionary or not self.continual_learning: + init_model = None + else: + init_model = self.dd.model_dictionary[dk.pair] + model = CatBoostRegressor( allow_writing_files=False, **self.model_training_parameters, ) - model.fit(X=train_data, eval_set=test_data) - - # some evidence that catboost pools have memory leaks: - # https://github.com/catboost/catboost/issues/1835 - del train_data, test_data - gc.collect() + model.fit(X=train_data, eval_set=test_data, init_model=init_model) return model diff --git a/freqtrade/freqai/prediction_models/CatboostRegressorMultiTarget.py b/freqtrade/freqai/prediction_models/CatboostRegressorMultiTarget.py index 9894decd1..9ed61488c 100644 --- a/freqtrade/freqai/prediction_models/CatboostRegressorMultiTarget.py +++ b/freqtrade/freqai/prediction_models/CatboostRegressorMultiTarget.py @@ -3,7 +3,7 @@ from typing import Any, Dict from catboost import CatBoostRegressor # , Pool from sklearn.multioutput import MultiOutputRegressor - +from freqtrade.freqai.data_kitchen import FreqaiDataKitchen from freqtrade.freqai.prediction_models.BaseRegressionModel import BaseRegressionModel @@ -17,7 +17,7 @@ class CatboostRegressorMultiTarget(BaseRegressionModel): has its own DataHandler where data is held, saved, loaded, and managed. """ - def fit(self, data_dictionary: Dict) -> Any: + def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen) -> Any: """ User sets up the training and test data to fit their desired model here :param data_dictionary: the dictionary constructed by DataHandler to hold @@ -34,6 +34,9 @@ class CatboostRegressorMultiTarget(BaseRegressionModel): eval_set = (data_dictionary["test_features"], data_dictionary["test_labels"]) sample_weight = data_dictionary["train_weights"] + if self.continual_learning: + logger.warning('Continual learning not supported for MultiTarget models') + model = MultiOutputRegressor(estimator=cbr) model.fit(X=X, y=y, sample_weight=sample_weight) # , eval_set=eval_set) diff --git a/freqtrade/freqai/prediction_models/LightGBMClassifier.py b/freqtrade/freqai/prediction_models/LightGBMClassifier.py index 4ac2c448b..0023a9f69 100644 --- a/freqtrade/freqai/prediction_models/LightGBMClassifier.py +++ b/freqtrade/freqai/prediction_models/LightGBMClassifier.py @@ -4,7 +4,7 @@ from typing import Any, Dict from lightgbm import LGBMClassifier from freqtrade.freqai.prediction_models.BaseClassifierModel import BaseClassifierModel - +from freqtrade.freqai.data_kitchen import FreqaiDataKitchen logger = logging.getLogger(__name__) @@ -16,7 +16,7 @@ class LightGBMClassifier(BaseClassifierModel): has its own DataHandler where data is held, saved, loaded, and managed. """ - def fit(self, data_dictionary: Dict) -> Any: + def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen) -> Any: """ User sets up the training and test data to fit their desired model here :params: @@ -35,9 +35,14 @@ class LightGBMClassifier(BaseClassifierModel): y = data_dictionary["train_labels"].to_numpy()[:, 0] train_weights = data_dictionary["train_weights"] + if dk.pair not in self.dd.model_dictionary or not self.continual_learning: + init_model = None + else: + init_model = self.dd.model_dictionary[dk.pair] + model = LGBMClassifier(**self.model_training_parameters) model.fit(X=X, y=y, eval_set=eval_set, sample_weight=train_weights, - eval_sample_weight=[test_weights]) + eval_sample_weight=[test_weights], init_model=init_model) return model diff --git a/freqtrade/freqai/prediction_models/LightGBMRegressor.py b/freqtrade/freqai/prediction_models/LightGBMRegressor.py index 2431fd2ad..81f0e6d22 100644 --- a/freqtrade/freqai/prediction_models/LightGBMRegressor.py +++ b/freqtrade/freqai/prediction_models/LightGBMRegressor.py @@ -4,7 +4,7 @@ from typing import Any, Dict from lightgbm import LGBMRegressor from freqtrade.freqai.prediction_models.BaseRegressionModel import BaseRegressionModel - +from freqtrade.freqai.data_kitchen import FreqaiDataKitchen logger = logging.getLogger(__name__) @@ -16,7 +16,7 @@ class LightGBMRegressor(BaseRegressionModel): has its own DataHandler where data is held, saved, loaded, and managed. """ - def fit(self, data_dictionary: Dict) -> Any: + def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen) -> Any: """ Most regressors use the same function names and arguments e.g. user can drop in LGBMRegressor in place of CatBoostRegressor and all data @@ -35,9 +35,14 @@ class LightGBMRegressor(BaseRegressionModel): y = data_dictionary["train_labels"] train_weights = data_dictionary["train_weights"] + if dk.pair not in self.dd.model_dictionary or not self.continual_learning: + init_model = None + else: + init_model = self.dd.model_dictionary[dk.pair] + model = LGBMRegressor(**self.model_training_parameters) model.fit(X=X, y=y, eval_set=eval_set, sample_weight=train_weights, - eval_sample_weight=[eval_weights]) + eval_sample_weight=[eval_weights], init_model=init_model) return model diff --git a/freqtrade/freqai/prediction_models/LightGBMRegressorMultiTarget.py b/freqtrade/freqai/prediction_models/LightGBMRegressorMultiTarget.py index ecd405369..2b25493e0 100644 --- a/freqtrade/freqai/prediction_models/LightGBMRegressorMultiTarget.py +++ b/freqtrade/freqai/prediction_models/LightGBMRegressorMultiTarget.py @@ -5,7 +5,7 @@ from lightgbm import LGBMRegressor from sklearn.multioutput import MultiOutputRegressor from freqtrade.freqai.prediction_models.BaseRegressionModel import BaseRegressionModel - +from freqtrade.freqai.data_kitchen import FreqaiDataKitchen logger = logging.getLogger(__name__) @@ -17,7 +17,7 @@ class LightGBMRegressorMultiTarget(BaseRegressionModel): has its own DataHandler where data is held, saved, loaded, and managed. """ - def fit(self, data_dictionary: Dict) -> Any: + def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen) -> Any: """ User sets up the training and test data to fit their desired model here :param data_dictionary: the dictionary constructed by DataHandler to hold @@ -31,6 +31,9 @@ class LightGBMRegressorMultiTarget(BaseRegressionModel): eval_set = (data_dictionary["test_features"], data_dictionary["test_labels"]) sample_weight = data_dictionary["train_weights"] + if self.continual_learning: + logger.warning('Continual learning not supported for MultiTarget models') + model = MultiOutputRegressor(estimator=lgb) model.fit(X=X, y=y, sample_weight=sample_weight) # , eval_set=eval_set) train_score = model.score(X, y) From 8d16dd804dcc0b0cfe94e1b3389186bc23d8e957 Mon Sep 17 00:00:00 2001 From: Wagner Costa Santos Date: Tue, 6 Sep 2022 15:42:47 -0300 Subject: [PATCH 04/33] hyperopt - freqai - docs and refactoring --- docs/freqai.md | 11 +++ freqtrade/configuration/configuration.py | 10 +++ freqtrade/freqai/data_drawer.py | 28 ++----- freqtrade/freqai/freqai_interface.py | 5 +- freqtrade/templates/FreqaiExampleStrategy.py | 86 +------------------- 5 files changed, 29 insertions(+), 111 deletions(-) diff --git a/docs/freqai.md b/docs/freqai.md index 303c2f151..f8cf64d21 100644 --- a/docs/freqai.md +++ b/docs/freqai.md @@ -279,6 +279,8 @@ The FreqAI strategy requires the user to include the following lines of code in Notice how the `populate_any_indicators()` is where the user adds their own features ([more information](#feature-engineering)) and labels ([more information](#setting-classifier-targets)). See a full example at `templates/FreqaiExampleStrategy.py`. +*Important*: The `self.freqai.start()` function cannot be called outside the `populate_indicators()`. + ### Setting the `startup_candle_count` Users need to take care to set the `startup_candle_count` in their strategy the same way they would for any normal Freqtrade strategy (see details [here](strategy-customization.md#strategy-startup-period)). This value is used by Freqtrade to ensure that a sufficient amount of data is provided when calling on the `dataprovider` to avoid any NaNs at the beginning of the first training. Users can easily set this value by identifying the longest period (in candle units) that they pass to their indicator creation functions (e.g. talib functions). In the present example, the user would pass 20 to as this value (since it is the maximum value in their `indicators_periods_candles`). @@ -532,6 +534,15 @@ for each pair, for each backtesting window within the expanded `--timerange`. --- +### Hyperopt + +The [Hyperopt](hyperopt.md) module can be executed with some restrictions: + +- The `--analyze-per-epoch` hyperopt parameter is not compatible with FreqAI. +- It's not possible to hyperopt indicators in `populate_any_indicators()` function. This means that the user cannot optimize model parameters using hyperopt. Apart from this exception, it is possible to optimize all other [spaces](hyperopt.md###runninghyperoptwithsmallersearchspace). +- The [Backtesting](#backtesting) instructions also apply apply to Hyperopt. + + ### Deciding the size of the sliding training window and backtesting duration The user defines the backtesting timerange with the typical `--timerange` parameter in the diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 7c68ac46c..d18b67ff2 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -285,6 +285,7 @@ class Configuration: logger.info('Parameter --stoplosses detected: %s ...', self.args["stoploss_range"]) # Hyperopt section + self._check_hyperopt_analyze_per_epoch_freqai() self._args_to_config(config, argname='hyperopt', logstring='Using Hyperopt class name: {}') @@ -537,3 +538,12 @@ class Configuration: config['pairs'] = load_file(pairs_file) if 'pairs' in config and isinstance(config['pairs'], list): config['pairs'].sort() + + def _check_hyperopt_analyze_per_epoch_freqai(self) -> None: + """ + Helper for block hyperopt with analyze-per-epoch param. + """ + if ("analyze_per_epoch" in self.args and + self.args["analyze_per_epoch"] and "freqaimodel" in self.args): + raise OperationalException('analyze-per-epoch parameter is \ + not allowed with a Freqai strategy.') diff --git a/freqtrade/freqai/data_drawer.py b/freqtrade/freqai/data_drawer.py index dff6b5942..9eeabef8f 100644 --- a/freqtrade/freqai/data_drawer.py +++ b/freqtrade/freqai/data_drawer.py @@ -93,16 +93,6 @@ class FreqaiDataDrawer: "model_filename": "", "trained_timestamp": 0, "priority": 1, "first": True, "data_path": "", "extras": {}} - def __getstate__(self): - """ - Return state values to be pickled. - It's necessary to allow serialization in hyperopt - """ - return ({ - "pair_dict": self.pair_dict, - "pair_dictionary_path": self.pair_dictionary_path - }) - def load_drawer_from_disk(self): """ Locate and load a previously saved data drawer full of all pair model metadata in @@ -165,22 +155,14 @@ class FreqaiDataDrawer: # create a backup shutil.copy(self.historic_predictions_path, self.historic_predictions_bkp_path) - def save_drawer_to_disk(self, live=False): + def save_drawer_to_disk(self): """ Save data drawer full of all pair model metadata in present model folder. """ - if live: - with self.save_lock: - with open(self.pair_dictionary_path, 'w') as fp: - rapidjson.dump( - self.pair_dict, fp, default=self.np_encoder, - number_mode=rapidjson.NM_NATIVE) - else: - # save_lock it's not working with hyperopt + with self.save_lock: with open(self.pair_dictionary_path, 'w') as fp: - rapidjson.dump( - self.pair_dict, fp, default=self.np_encoder, - number_mode=rapidjson.NM_NATIVE) + rapidjson.dump(self.pair_dict, fp, default=self.np_encoder, + number_mode=rapidjson.NM_NATIVE) def save_follower_dict_to_disk(self): """ @@ -455,7 +437,7 @@ class FreqaiDataDrawer: self.model_dictionary[coin] = model self.pair_dict[coin]["model_filename"] = dk.model_filename self.pair_dict[coin]["data_path"] = str(dk.data_path) - self.save_drawer_to_disk(dk.live) + self.save_drawer_to_disk() return diff --git a/freqtrade/freqai/freqai_interface.py b/freqtrade/freqai/freqai_interface.py index 9a05e8383..f631c9126 100644 --- a/freqtrade/freqai/freqai_interface.py +++ b/freqtrade/freqai/freqai_interface.py @@ -92,10 +92,9 @@ class IFreqaiModel(ABC): def __getstate__(self): """ - Return state values to be pickled. - It's necessary to allow serialization in hyperopt + Return an empty state to be pickled in hyperopt """ - return ({"dd": self.dd}) + return ({}) def assert_config(self, config: Dict[str, Any]) -> None: diff --git a/freqtrade/templates/FreqaiExampleStrategy.py b/freqtrade/templates/FreqaiExampleStrategy.py index 0e822a028..78132b06d 100644 --- a/freqtrade/templates/FreqaiExampleStrategy.py +++ b/freqtrade/templates/FreqaiExampleStrategy.py @@ -6,9 +6,7 @@ import talib.abstract as ta from pandas import DataFrame from technical import qtpylib -from freqtrade.exchange import timeframe_to_prev_date -from freqtrade.persistence import Trade -from freqtrade.strategy import DecimalParameter, IntParameter, IStrategy, merge_informative_pair +from freqtrade.strategy import IStrategy, merge_informative_pair logger = logging.getLogger(__name__) @@ -47,11 +45,6 @@ class FreqaiExampleStrategy(IStrategy): startup_candle_count: int = 40 can_short = False - linear_roi_offset = DecimalParameter( - 0.00, 0.02, default=0.005, space="sell", optimize=False, load=True - ) - max_roi_time_long = IntParameter(0, 800, default=400, space="sell", optimize=False, load=True) - def informative_pairs(self): whitelist_pairs = self.dp.current_whitelist() corr_pairs = self.config["freqai"]["feature_parameters"]["include_corr_pairlist"] @@ -226,83 +219,6 @@ class FreqaiExampleStrategy(IStrategy): def get_ticker_indicator(self): return int(self.config["timeframe"][:-1]) - def custom_exit( - self, pair: str, trade: Trade, current_time, current_rate, current_profit, **kwargs - ): - - dataframe, _ = self.dp.get_analyzed_dataframe(pair=pair, timeframe=self.timeframe) - - trade_date = timeframe_to_prev_date(self.config["timeframe"], trade.open_date_utc) - trade_candle = dataframe.loc[(dataframe["date"] == trade_date)] - - if trade_candle.empty: - return None - trade_candle = trade_candle.squeeze() - - follow_mode = self.config.get("freqai", {}).get("follow_mode", False) - - if not follow_mode: - pair_dict = self.freqai.dd.pair_dict - else: - pair_dict = self.freqai.dd.follower_dict - - entry_tag = trade.enter_tag - - if ( - "prediction" + entry_tag not in pair_dict[pair] - or pair_dict[pair]['extras']["prediction" + entry_tag] == 0 - ): - pair_dict[pair]['extras']["prediction" + entry_tag] = abs(trade_candle["&-s_close"]) - if not follow_mode: - self.freqai.dd.save_drawer_to_disk() - else: - self.freqai.dd.save_follower_dict_to_disk() - - roi_price = pair_dict[pair]['extras']["prediction" + entry_tag] - roi_time = self.max_roi_time_long.value - - roi_decay = roi_price * ( - 1 - ((current_time - trade.open_date_utc).seconds) / (roi_time * 60) - ) - if roi_decay < 0: - roi_decay = self.linear_roi_offset.value - else: - roi_decay += self.linear_roi_offset.value - - if current_profit > roi_decay: - return "roi_custom_win" - - if current_profit < -roi_decay: - return "roi_custom_loss" - - def confirm_trade_exit( - self, - pair: str, - trade: Trade, - order_type: str, - amount: float, - rate: float, - time_in_force: str, - exit_reason: str, - current_time, - **kwargs, - ) -> bool: - - entry_tag = trade.enter_tag - follow_mode = self.config.get("freqai", {}).get("follow_mode", False) - if not follow_mode: - pair_dict = self.freqai.dd.pair_dict - else: - pair_dict = self.freqai.dd.follower_dict - - pair_dict[pair]['extras']["prediction" + entry_tag] = 0 - if not follow_mode: - self.freqai.dd.save_drawer_to_disk() - else: - self.freqai.dd.save_follower_dict_to_disk() - - return True - def confirm_trade_entry( self, pair: str, From 5aba5de20f5dea1f61aa7ced19a966744b5c3e30 Mon Sep 17 00:00:00 2001 From: Wagner Costa Santos Date: Tue, 6 Sep 2022 16:17:10 -0300 Subject: [PATCH 05/33] fix link - hyperopt spaces --- docs/freqai.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/freqai.md b/docs/freqai.md index fa58feb07..7aabb0e56 100644 --- a/docs/freqai.md +++ b/docs/freqai.md @@ -539,7 +539,7 @@ for each pair, for each backtesting window within the expanded `--timerange`. The [Hyperopt](hyperopt.md) module can be executed with some restrictions: - The `--analyze-per-epoch` hyperopt parameter is not compatible with FreqAI. -- It's not possible to hyperopt indicators in `populate_any_indicators()` function. This means that the user cannot optimize model parameters using hyperopt. Apart from this exception, it is possible to optimize all other [spaces](hyperopt.md###runninghyperoptwithsmallersearchspace). +- It's not possible to hyperopt indicators in `populate_any_indicators()` function. This means that the user cannot optimize model parameters using hyperopt. Apart from this exception, it is possible to optimize all other [spaces](hyperopt.md#running-hyperopt-with-smaller-search-space). - The [Backtesting](#backtesting) instructions also apply apply to Hyperopt. From 972b6991057d00ea1d321403cebf8e2feefa57ff Mon Sep 17 00:00:00 2001 From: Wagner Costa Santos Date: Wed, 7 Sep 2022 11:11:31 -0300 Subject: [PATCH 06/33] hyperopt - freqai - change validation to config_validation --- freqtrade/configuration/config_validation.py | 9 +++++++++ freqtrade/configuration/configuration.py | 10 ---------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/freqtrade/configuration/config_validation.py b/freqtrade/configuration/config_validation.py index ee846e7e6..b37d44d0f 100644 --- a/freqtrade/configuration/config_validation.py +++ b/freqtrade/configuration/config_validation.py @@ -84,6 +84,7 @@ def validate_config_consistency(conf: Dict[str, Any], preliminary: bool = False) _validate_protections(conf) _validate_unlimited_amount(conf) _validate_ask_orderbook(conf) + _validate_freqai_hyperopt(conf) validate_migrated_strategy_settings(conf) # validate configuration before returning @@ -323,6 +324,14 @@ def _validate_pricing_rules(conf: Dict[str, Any]) -> None: del conf['ask_strategy'] +def _validate_freqai_hyperopt(conf: Dict[str, Any]) -> None: + freqaimodel = conf.get('freqaimodel') + analyze_per_epoch = conf.get('analyze_per_epoch', False) + if analyze_per_epoch and freqaimodel is not None: + raise OperationalException( + 'Using analyze-per-epoch parameter is not supported with a FreqAI strategy.') + + def _strategy_settings(conf: Dict[str, Any]) -> None: process_deprecated_setting(conf, None, 'use_sell_signal', None, 'use_exit_signal') diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index d18b67ff2..7c68ac46c 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -285,7 +285,6 @@ class Configuration: logger.info('Parameter --stoplosses detected: %s ...', self.args["stoploss_range"]) # Hyperopt section - self._check_hyperopt_analyze_per_epoch_freqai() self._args_to_config(config, argname='hyperopt', logstring='Using Hyperopt class name: {}') @@ -538,12 +537,3 @@ class Configuration: config['pairs'] = load_file(pairs_file) if 'pairs' in config and isinstance(config['pairs'], list): config['pairs'].sort() - - def _check_hyperopt_analyze_per_epoch_freqai(self) -> None: - """ - Helper for block hyperopt with analyze-per-epoch param. - """ - if ("analyze_per_epoch" in self.args and - self.args["analyze_per_epoch"] and "freqaimodel" in self.args): - raise OperationalException('analyze-per-epoch parameter is \ - not allowed with a Freqai strategy.') From 047ded1baa7c7e980b98c8289f689e2f900cf203 Mon Sep 17 00:00:00 2001 From: th0rntwig Date: Wed, 7 Sep 2022 17:47:27 +0200 Subject: [PATCH 07/33] Check for constant columns --- freqtrade/freqai/data_kitchen.py | 41 ++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/freqtrade/freqai/data_kitchen.py b/freqtrade/freqai/data_kitchen.py index 7670cfd45..0a2355c48 100644 --- a/freqtrade/freqai/data_kitchen.py +++ b/freqtrade/freqai/data_kitchen.py @@ -184,7 +184,7 @@ class FreqaiDataKitchen: def filter_features( self, - unfiltered_dataframe: DataFrame, + unfiltered_df: DataFrame, training_feature_list: List, label_list: List = list(), training_filter: bool = True, @@ -195,31 +195,36 @@ class FreqaiDataKitchen: 0s in the prediction dataset. However, prediction dataset do_predict will reflect any row that had a NaN and will shield user from that prediction. :params: - :unfiltered_dataframe: the full dataframe for the present training period + :unfiltered_df: the full dataframe for the present training period :training_feature_list: list, the training feature list constructed by self.build_feature_list() according to user specified parameters in the configuration file. :labels: the labels for the dataset :training_filter: boolean which lets the function know if it is training data or prediction data to be filtered. :returns: - :filtered_dataframe: dataframe cleaned of NaNs and only containing the user + :filtered_df: dataframe cleaned of NaNs and only containing the user requested feature set. :labels: labels cleaned of NaNs. """ - filtered_dataframe = unfiltered_dataframe.filter(training_feature_list, axis=1) - filtered_dataframe = filtered_dataframe.replace([np.inf, -np.inf], np.nan) + filtered_df = unfiltered_df.filter(training_feature_list, axis=1) + filtered_df = filtered_df.replace([np.inf, -np.inf], np.nan) - drop_index = pd.isnull(filtered_dataframe).any(1) # get the rows that have NaNs, + const_cols = filtered_df[:, filtered_df.nunique(axis=0) == len(filtered_df.index)].columns + if const_cols: + filtered_df = filtered_df.filter(filtered_df.columns.difference(const_cols)) + logger.warning(f"Removed features {const_cols} with constant values.") + + drop_index = pd.isnull(filtered_df).any(1) # get the rows that have NaNs, drop_index = drop_index.replace(True, 1).replace(False, 0) # pep8 requirement. if (training_filter): # we don't care about total row number (total no. datapoints) in training, we only care # about removing any row with NaNs # if labels has multiple columns (user wants to train multiple modelEs), we detect here - labels = unfiltered_dataframe.filter(label_list, axis=1) + labels = unfiltered_df.filter(label_list, axis=1) drop_index_labels = pd.isnull(labels).any(1) drop_index_labels = drop_index_labels.replace(True, 1).replace(False, 0) - dates = unfiltered_dataframe['date'] - filtered_dataframe = filtered_dataframe[ + dates = unfiltered_df['date'] + filtered_df = filtered_df[ (drop_index == 0) & (drop_index_labels == 0) ] # dropping values labels = labels[ @@ -229,13 +234,13 @@ class FreqaiDataKitchen: (drop_index == 0) & (drop_index_labels == 0) ] logger.info( - f"dropped {len(unfiltered_dataframe) - len(filtered_dataframe)} training points" - f" due to NaNs in populated dataset {len(unfiltered_dataframe)}." + f"dropped {len(unfiltered_df) - len(filtered_df)} training points" + f" due to NaNs in populated dataset {len(unfiltered_df)}." ) - if (1 - len(filtered_dataframe) / len(unfiltered_dataframe)) > 0.1 and self.live: - worst_indicator = str(unfiltered_dataframe.count().idxmin()) + if (1 - len(filtered_df) / len(unfiltered_df)) > 0.1 and self.live: + worst_indicator = str(unfiltered_df.count().idxmin()) logger.warning( - f" {(1 - len(filtered_dataframe)/len(unfiltered_dataframe)) * 100:.0f} percent " + f" {(1 - len(filtered_df)/len(unfiltered_df)) * 100:.0f} percent " " of training data dropped due to NaNs, model may perform inconsistent " f"with expectations. Verify {worst_indicator}" ) @@ -244,9 +249,9 @@ class FreqaiDataKitchen: else: # we are backtesting so we need to preserve row number to send back to strategy, # so now we use do_predict to avoid any prediction based on a NaN - drop_index = pd.isnull(filtered_dataframe).any(1) + drop_index = pd.isnull(filtered_df).any(1) self.data["filter_drop_index_prediction"] = drop_index - filtered_dataframe.fillna(0, inplace=True) + filtered_df.fillna(0, inplace=True) # replacing all NaNs with zeros to avoid issues in 'prediction', but any prediction # that was based on a single NaN is ultimately protected from buys with do_predict drop_index = ~drop_index @@ -255,11 +260,11 @@ class FreqaiDataKitchen: logger.info( "dropped %s of %s prediction data points due to NaNs.", len(self.do_predict) - self.do_predict.sum(), - len(filtered_dataframe), + len(filtered_df), ) labels = [] - return filtered_dataframe, labels + return filtered_df, labels def build_data_dictionary( self, From cdc72bf8cab4bf78b2da460690a3b71ab076bff4 Mon Sep 17 00:00:00 2001 From: th0rntwig Date: Wed, 7 Sep 2022 18:14:13 +0200 Subject: [PATCH 08/33] Correct indexing --- freqtrade/freqai/data_kitchen.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/freqtrade/freqai/data_kitchen.py b/freqtrade/freqai/data_kitchen.py index 0a2355c48..790b3c078 100644 --- a/freqtrade/freqai/data_kitchen.py +++ b/freqtrade/freqai/data_kitchen.py @@ -209,7 +209,7 @@ class FreqaiDataKitchen: filtered_df = unfiltered_df.filter(training_feature_list, axis=1) filtered_df = filtered_df.replace([np.inf, -np.inf], np.nan) - const_cols = filtered_df[:, filtered_df.nunique(axis=0) == len(filtered_df.index)].columns + const_cols = list((filtered_df.nunique() == 1).loc[lambda x: x].index) if const_cols: filtered_df = filtered_df.filter(filtered_df.columns.difference(const_cols)) logger.warning(f"Removed features {const_cols} with constant values.") @@ -1205,7 +1205,6 @@ class FreqaiDataKitchen: def save_backtesting_prediction( self, append_df: DataFrame ) -> None: - """ Save prediction dataframe from backtesting to h5 file format :param append_df: dataframe for backtesting period @@ -1219,7 +1218,6 @@ class FreqaiDataKitchen: def get_backtesting_prediction( self ) -> DataFrame: - """ Get prediction dataframe from h5 file format """ From 4c9ac6b7c0959a7d596279a1388145f8a90bd8da Mon Sep 17 00:00:00 2001 From: robcaulk Date: Wed, 7 Sep 2022 18:58:55 +0200 Subject: [PATCH 09/33] add kwargs, reduce duplicated code --- freqtrade/freqai/freqai_interface.py | 15 ++++++++++++--- .../prediction_models/CatboostClassifier.py | 8 +++----- .../freqai/prediction_models/CatboostRegressor.py | 10 +++------- .../CatboostRegressorMultiTarget.py | 3 ++- .../prediction_models/LightGBMClassifier.py | 10 ++++------ .../freqai/prediction_models/LightGBMRegressor.py | 10 ++++------ .../LightGBMRegressorMultiTarget.py | 5 +++-- 7 files changed, 31 insertions(+), 30 deletions(-) diff --git a/freqtrade/freqai/freqai_interface.py b/freqtrade/freqai/freqai_interface.py index b6f3d8ebc..101df88ec 100644 --- a/freqtrade/freqai/freqai_interface.py +++ b/freqtrade/freqai/freqai_interface.py @@ -661,11 +661,20 @@ class IFreqaiModel(ABC): self.train_time = 0 return + def get_init_model(self, pair: str) -> Any: + if pair not in self.dd.model_dictionary or not self.continual_learning: + init_model = None + else: + init_model = self.dd.model_dictionary[pair] + + return init_model + # Following methods which are overridden by user made prediction models. # See freqai/prediction_models/CatboostPredictionModel.py for an example. @abstractmethod - def train(self, unfiltered_dataframe: DataFrame, pair: str, dk: FreqaiDataKitchen) -> Any: + def train(self, unfiltered_dataframe: DataFrame, pair: str, + dk: FreqaiDataKitchen, **kwargs) -> Any: """ Filter the training data and train a model to it. Train makes heavy use of the datahandler for storing, saving, loading, and analyzing the data. @@ -675,7 +684,7 @@ class IFreqaiModel(ABC): """ @abstractmethod - def fit(self, data_dictionary: Dict[str, Any], dk: FreqaiDataKitchen) -> Any: + def fit(self, data_dictionary: Dict[str, Any], dk: FreqaiDataKitchen, **kwargs) -> Any: """ Most regressors use the same function names and arguments e.g. user can drop in LGBMRegressor in place of CatBoostRegressor and all data @@ -688,7 +697,7 @@ class IFreqaiModel(ABC): @abstractmethod def predict( - self, dataframe: DataFrame, dk: FreqaiDataKitchen, first: bool = True + self, dataframe: DataFrame, dk: FreqaiDataKitchen, first: bool = True, **kwargs ) -> Tuple[DataFrame, NDArray[np.int_]]: """ Filter the prediction features data and predict with it. diff --git a/freqtrade/freqai/prediction_models/CatboostClassifier.py b/freqtrade/freqai/prediction_models/CatboostClassifier.py index 13395879a..cd7afd392 100644 --- a/freqtrade/freqai/prediction_models/CatboostClassifier.py +++ b/freqtrade/freqai/prediction_models/CatboostClassifier.py @@ -2,6 +2,7 @@ import logging from typing import Any, Dict from catboost import CatBoostClassifier, Pool + from freqtrade.freqai.data_kitchen import FreqaiDataKitchen from freqtrade.freqai.prediction_models.BaseClassifierModel import BaseClassifierModel @@ -16,7 +17,7 @@ class CatboostClassifier(BaseClassifierModel): has its own DataHandler where data is held, saved, loaded, and managed. """ - def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen) -> Any: + def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ User sets up the training and test data to fit their desired model here :params: @@ -36,10 +37,7 @@ class CatboostClassifier(BaseClassifierModel): **self.model_training_parameters, ) - if dk.pair not in self.dd.model_dictionary or not self.continual_learning: - init_model = None - else: - init_model = self.dd.model_dictionary[dk.pair] + init_model = self.get_init_model(dk.pair) cbr.fit(train_data, init_model=init_model) diff --git a/freqtrade/freqai/prediction_models/CatboostRegressor.py b/freqtrade/freqai/prediction_models/CatboostRegressor.py index 0b8bc162b..1ce31b628 100644 --- a/freqtrade/freqai/prediction_models/CatboostRegressor.py +++ b/freqtrade/freqai/prediction_models/CatboostRegressor.py @@ -1,10 +1,9 @@ -import gc import logging from typing import Any, Dict from catboost import CatBoostRegressor, Pool -from freqtrade.freqai.data_kitchen import FreqaiDataKitchen +from freqtrade.freqai.data_kitchen import FreqaiDataKitchen from freqtrade.freqai.prediction_models.BaseRegressionModel import BaseRegressionModel @@ -18,7 +17,7 @@ class CatboostRegressor(BaseRegressionModel): has its own DataHandler where data is held, saved, loaded, and managed. """ - def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen) -> Any: + def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ User sets up the training and test data to fit their desired model here :param data_dictionary: the dictionary constructed by DataHandler to hold @@ -39,10 +38,7 @@ class CatboostRegressor(BaseRegressionModel): weight=data_dictionary["test_weights"], ) - if dk.pair not in self.dd.model_dictionary or not self.continual_learning: - init_model = None - else: - init_model = self.dd.model_dictionary[dk.pair] + init_model = self.get_init_model(dk.pair) model = CatBoostRegressor( allow_writing_files=False, diff --git a/freqtrade/freqai/prediction_models/CatboostRegressorMultiTarget.py b/freqtrade/freqai/prediction_models/CatboostRegressorMultiTarget.py index 9ed61488c..bc52bfdd9 100644 --- a/freqtrade/freqai/prediction_models/CatboostRegressorMultiTarget.py +++ b/freqtrade/freqai/prediction_models/CatboostRegressorMultiTarget.py @@ -3,6 +3,7 @@ from typing import Any, Dict from catboost import CatBoostRegressor # , Pool from sklearn.multioutput import MultiOutputRegressor + from freqtrade.freqai.data_kitchen import FreqaiDataKitchen from freqtrade.freqai.prediction_models.BaseRegressionModel import BaseRegressionModel @@ -17,7 +18,7 @@ class CatboostRegressorMultiTarget(BaseRegressionModel): has its own DataHandler where data is held, saved, loaded, and managed. """ - def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen) -> Any: + def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ User sets up the training and test data to fit their desired model here :param data_dictionary: the dictionary constructed by DataHandler to hold diff --git a/freqtrade/freqai/prediction_models/LightGBMClassifier.py b/freqtrade/freqai/prediction_models/LightGBMClassifier.py index 0023a9f69..69867eae3 100644 --- a/freqtrade/freqai/prediction_models/LightGBMClassifier.py +++ b/freqtrade/freqai/prediction_models/LightGBMClassifier.py @@ -3,8 +3,9 @@ from typing import Any, Dict from lightgbm import LGBMClassifier -from freqtrade.freqai.prediction_models.BaseClassifierModel import BaseClassifierModel from freqtrade.freqai.data_kitchen import FreqaiDataKitchen +from freqtrade.freqai.prediction_models.BaseClassifierModel import BaseClassifierModel + logger = logging.getLogger(__name__) @@ -16,7 +17,7 @@ class LightGBMClassifier(BaseClassifierModel): has its own DataHandler where data is held, saved, loaded, and managed. """ - def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen) -> Any: + def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ User sets up the training and test data to fit their desired model here :params: @@ -35,10 +36,7 @@ class LightGBMClassifier(BaseClassifierModel): y = data_dictionary["train_labels"].to_numpy()[:, 0] train_weights = data_dictionary["train_weights"] - if dk.pair not in self.dd.model_dictionary or not self.continual_learning: - init_model = None - else: - init_model = self.dd.model_dictionary[dk.pair] + init_model = self.get_init_model(dk.pair) model = LGBMClassifier(**self.model_training_parameters) diff --git a/freqtrade/freqai/prediction_models/LightGBMRegressor.py b/freqtrade/freqai/prediction_models/LightGBMRegressor.py index 81f0e6d22..99e9ff887 100644 --- a/freqtrade/freqai/prediction_models/LightGBMRegressor.py +++ b/freqtrade/freqai/prediction_models/LightGBMRegressor.py @@ -3,8 +3,9 @@ from typing import Any, Dict from lightgbm import LGBMRegressor -from freqtrade.freqai.prediction_models.BaseRegressionModel import BaseRegressionModel from freqtrade.freqai.data_kitchen import FreqaiDataKitchen +from freqtrade.freqai.prediction_models.BaseRegressionModel import BaseRegressionModel + logger = logging.getLogger(__name__) @@ -16,7 +17,7 @@ class LightGBMRegressor(BaseRegressionModel): has its own DataHandler where data is held, saved, loaded, and managed. """ - def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen) -> Any: + def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ Most regressors use the same function names and arguments e.g. user can drop in LGBMRegressor in place of CatBoostRegressor and all data @@ -35,10 +36,7 @@ class LightGBMRegressor(BaseRegressionModel): y = data_dictionary["train_labels"] train_weights = data_dictionary["train_weights"] - if dk.pair not in self.dd.model_dictionary or not self.continual_learning: - init_model = None - else: - init_model = self.dd.model_dictionary[dk.pair] + init_model = self.get_init_model(dk.pair) model = LGBMRegressor(**self.model_training_parameters) diff --git a/freqtrade/freqai/prediction_models/LightGBMRegressorMultiTarget.py b/freqtrade/freqai/prediction_models/LightGBMRegressorMultiTarget.py index 2b25493e0..c34680dbe 100644 --- a/freqtrade/freqai/prediction_models/LightGBMRegressorMultiTarget.py +++ b/freqtrade/freqai/prediction_models/LightGBMRegressorMultiTarget.py @@ -4,8 +4,9 @@ from typing import Any, Dict from lightgbm import LGBMRegressor from sklearn.multioutput import MultiOutputRegressor -from freqtrade.freqai.prediction_models.BaseRegressionModel import BaseRegressionModel from freqtrade.freqai.data_kitchen import FreqaiDataKitchen +from freqtrade.freqai.prediction_models.BaseRegressionModel import BaseRegressionModel + logger = logging.getLogger(__name__) @@ -17,7 +18,7 @@ class LightGBMRegressorMultiTarget(BaseRegressionModel): has its own DataHandler where data is held, saved, loaded, and managed. """ - def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen) -> Any: + def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ User sets up the training and test data to fit their desired model here :param data_dictionary: the dictionary constructed by DataHandler to hold From bf3ee51167f183739b0a71677126b9c18d733560 Mon Sep 17 00:00:00 2001 From: Wagner Costa Santos Date: Wed, 7 Sep 2022 14:42:05 -0300 Subject: [PATCH 10/33] validate freqai hyperopt with freqai enabled param --- freqtrade/configuration/config_validation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/configuration/config_validation.py b/freqtrade/configuration/config_validation.py index b37d44d0f..8d9112bef 100644 --- a/freqtrade/configuration/config_validation.py +++ b/freqtrade/configuration/config_validation.py @@ -325,9 +325,9 @@ def _validate_pricing_rules(conf: Dict[str, Any]) -> None: def _validate_freqai_hyperopt(conf: Dict[str, Any]) -> None: - freqaimodel = conf.get('freqaimodel') + freqai_enabled = conf.get('freqai', {}).get('enabled', False) analyze_per_epoch = conf.get('analyze_per_epoch', False) - if analyze_per_epoch and freqaimodel is not None: + if analyze_per_epoch and freqai_enabled: raise OperationalException( 'Using analyze-per-epoch parameter is not supported with a FreqAI strategy.') From f4f2884a66c97c05e6871b1ecf2f12b1bc0ec460 Mon Sep 17 00:00:00 2001 From: Wagner Costa Santos Date: Wed, 7 Sep 2022 18:52:58 -0300 Subject: [PATCH 11/33] Fix freqai backtesting time range issue --- freqtrade/freqai/data_kitchen.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/freqtrade/freqai/data_kitchen.py b/freqtrade/freqai/data_kitchen.py index 7670cfd45..01b2f1f3b 100644 --- a/freqtrade/freqai/data_kitchen.py +++ b/freqtrade/freqai/data_kitchen.py @@ -461,6 +461,20 @@ class FreqaiDataKitchen: return df + def remove_training_from_backtesting( + self + ) -> DataFrame: + """ + Function which takes the backtesting time range and + remove training data from dataframe + """ + tr = self.config["timerange"] + backtesting_timerange = TimeRange.parse_timerange(tr) + start = datetime.fromtimestamp(backtesting_timerange.startts, tz=timezone.utc) + df = self.return_dataframe + df = df.loc[df["date"] >= start, :] + return df + def principal_component_analysis(self) -> None: """ Performs Principal Component Analysis on the data for dimensionality reduction @@ -954,6 +968,7 @@ class FreqaiDataKitchen: to_keep = [col for col in dataframe.columns if not col.startswith("&")] self.return_dataframe = pd.concat([dataframe[to_keep], self.full_df], axis=1) + self.return_dataframe = self.remove_training_from_backtesting() self.full_df = DataFrame() return From bc7295579ff89387a402a6255ff5acfa9c583231 Mon Sep 17 00:00:00 2001 From: robcaulk Date: Thu, 8 Sep 2022 22:22:50 +0200 Subject: [PATCH 12/33] improve docs, make example strat hyperoptable --- docs/freqai.md | 22 ++++++++++++-- freqtrade/templates/FreqaiExampleStrategy.py | 31 +++++++++++++------- 2 files changed, 39 insertions(+), 14 deletions(-) diff --git a/docs/freqai.md b/docs/freqai.md index 006fe1393..92e019dfe 100644 --- a/docs/freqai.md +++ b/docs/freqai.md @@ -538,12 +538,28 @@ for each pair, for each backtesting window within the expanded `--timerange`. ### Hyperopt -The [Hyperopt](hyperopt.md) module can be executed with some restrictions: +Users can hyperopt using the same command as typical [hyperopt](hyperopt.md): + +```bash +freqtrade hyperopt --hyperopt-loss SharpeHyperOptLoss --strategy FreqaiExampleStrategy --strategy-path freqtrade/templates --config config_examples/config_freqai.example.json --timerange 20220428-20220507 +``` + +Users need to have the data pre-downloaded in the same fashion as if they were doing a FreqAI [backtest](#backtesting). In addition, users must consider some restrictions when trying to [Hyperopt](hyperopt.md) FreqAI strategies: - The `--analyze-per-epoch` hyperopt parameter is not compatible with FreqAI. - It's not possible to hyperopt indicators in `populate_any_indicators()` function. This means that the user cannot optimize model parameters using hyperopt. Apart from this exception, it is possible to optimize all other [spaces](hyperopt.md#running-hyperopt-with-smaller-search-space). -- The [Backtesting](#backtesting) instructions also apply apply to Hyperopt. - +- The [Backtesting](#backtesting) instructions also apply to Hyperopt. + +The best method for combining hyperopt and FreqAI is to focus on hyperopting entry/exit thresholds/criteria. Users need to focus on hyperopting parameters that are not used in their FreqAI features. For example, users should not try to hyperopt rolling window lengths in their feature creation, or any of their FreqAI config which changes predictions. In order to efficiently hyperopt the FreqAI strategy, FreqAI stores predictions as dataframes and reuses them. Hence the requirement to hyperopt entry/exit thresholds/criteria only. + +A good example of a hyperoptable parameter in FreqAI is a value for `DI_values` beyond which we consider outliers and below which we consider inliers: + +```python +di_max = IntParameter(low=1, high=20, default=10, space='buy', optimize=True, load=True) +dataframe['outlier'] = np.where(dataframe['DI_values'] > self.di_max.value/10, 1, 0) +``` + +Which would help the user understand the appropriate Dissimilarity Index values for their particular parameter space. ### Deciding the size of the sliding training window and backtesting duration diff --git a/freqtrade/templates/FreqaiExampleStrategy.py b/freqtrade/templates/FreqaiExampleStrategy.py index 78132b06d..8b8e03af7 100644 --- a/freqtrade/templates/FreqaiExampleStrategy.py +++ b/freqtrade/templates/FreqaiExampleStrategy.py @@ -6,7 +6,7 @@ import talib.abstract as ta from pandas import DataFrame from technical import qtpylib -from freqtrade.strategy import IStrategy, merge_informative_pair +from freqtrade.strategy import IStrategy, merge_informative_pair, CategoricalParameter logger = logging.getLogger(__name__) @@ -29,9 +29,6 @@ class FreqaiExampleStrategy(IStrategy): "main_plot": {}, "subplots": { "prediction": {"prediction": {"color": "blue"}}, - "target_roi": { - "target_roi": {"color": "brown"}, - }, "do_predict": { "do_predict": {"color": "brown"}, }, @@ -45,6 +42,11 @@ class FreqaiExampleStrategy(IStrategy): startup_candle_count: int = 40 can_short = False + std_dev_multiplier_buy = CategoricalParameter( + [0.75, 1, 1.25, 1.5, 1.75], default=1.25, space="buy", optimize=True) + std_dev_multiplier_sell = CategoricalParameter( + [0.1, 0.25, 0.4], space="sell", default=0.2, optimize=True) + def informative_pairs(self): whitelist_pairs = self.dp.current_whitelist() corr_pairs = self.config["freqai"]["feature_parameters"]["include_corr_pairlist"] @@ -182,21 +184,26 @@ class FreqaiExampleStrategy(IStrategy): # `populate_any_indicators()` for each training period. dataframe = self.freqai.start(dataframe, metadata, self) - - dataframe["target_roi"] = dataframe["&-s_close_mean"] + dataframe["&-s_close_std"] * 1.25 - dataframe["sell_roi"] = dataframe["&-s_close_mean"] - dataframe["&-s_close_std"] * 1.25 + for val in self.std_dev_multiplier_buy.range: + dataframe[f'target_roi_{val}'] = dataframe["&-s_close_mean"] + \ + dataframe["&-s_close_std"] * val + for val in self.std_dev_multiplier_sell.range: + dataframe[f'sell_roi_{val}'] = dataframe["&-s_close_mean"] - \ + dataframe["&-s_close_std"] * val return dataframe def populate_entry_trend(self, df: DataFrame, metadata: dict) -> DataFrame: - enter_long_conditions = [df["do_predict"] == 1, df["&-s_close"] > df["target_roi"]] + enter_long_conditions = [df["do_predict"] == 1, df["&-s_close"] + > df[f"target_roi_{self.std_dev_multiplier_buy.value}"]] if enter_long_conditions: df.loc[ reduce(lambda x, y: x & y, enter_long_conditions), ["enter_long", "enter_tag"] ] = (1, "long") - enter_short_conditions = [df["do_predict"] == 1, df["&-s_close"] < df["sell_roi"]] + enter_short_conditions = [df["do_predict"] == 1, df["&-s_close"] + < df[f"sell_roi_{self.std_dev_multiplier_sell.value}"]] if enter_short_conditions: df.loc[ @@ -206,11 +213,13 @@ class FreqaiExampleStrategy(IStrategy): return df def populate_exit_trend(self, df: DataFrame, metadata: dict) -> DataFrame: - exit_long_conditions = [df["do_predict"] == 1, df["&-s_close"] < df["sell_roi"] * 0.25] + exit_long_conditions = [df["do_predict"] == 1, df["&-s_close"] < + df[f"sell_roi_{self.std_dev_multiplier_sell.value}"] * 0.25] if exit_long_conditions: df.loc[reduce(lambda x, y: x & y, exit_long_conditions), "exit_long"] = 1 - exit_short_conditions = [df["do_predict"] == 1, df["&-s_close"] > df["target_roi"] * 0.25] + exit_short_conditions = [df["do_predict"] == 1, df["&-s_close"] > + df[f"target_roi_{self.std_dev_multiplier_buy.value}"] * 0.25] if exit_short_conditions: df.loc[reduce(lambda x, y: x & y, exit_short_conditions), "exit_short"] = 1 From c5d918075881f62277f21854fdbd85fd94335024 Mon Sep 17 00:00:00 2001 From: robcaulk Date: Thu, 8 Sep 2022 22:35:52 +0200 Subject: [PATCH 13/33] isort --- freqtrade/templates/FreqaiExampleStrategy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/templates/FreqaiExampleStrategy.py b/freqtrade/templates/FreqaiExampleStrategy.py index 8b8e03af7..991792eb9 100644 --- a/freqtrade/templates/FreqaiExampleStrategy.py +++ b/freqtrade/templates/FreqaiExampleStrategy.py @@ -6,7 +6,7 @@ import talib.abstract as ta from pandas import DataFrame from technical import qtpylib -from freqtrade.strategy import IStrategy, merge_informative_pair, CategoricalParameter +from freqtrade.strategy import CategoricalParameter, IStrategy, merge_informative_pair logger = logging.getLogger(__name__) From 92d71ebdb730c16bcab4eb61e3516ea7a58e4825 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 9 Sep 2022 03:03:30 +0000 Subject: [PATCH 14/33] Bump python from 3.10.6-slim-bullseye to 3.10.7-slim-bullseye Bumps python from 3.10.6-slim-bullseye to 3.10.7-slim-bullseye. --- updated-dependencies: - dependency-name: python dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index e84a4d095..b3e5d5e88 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.10.6-slim-bullseye as base +FROM python:3.10.7-slim-bullseye as base # Setup env ENV LANG C.UTF-8 From d38cc061396d615832833fcbc880be5bdced8459 Mon Sep 17 00:00:00 2001 From: Wagner Costa Santos Date: Wed, 7 Sep 2022 18:52:58 -0300 Subject: [PATCH 15/33] Fix freqai backtesting time range issue --- freqtrade/freqai/data_kitchen.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/freqtrade/freqai/data_kitchen.py b/freqtrade/freqai/data_kitchen.py index 7670cfd45..01b2f1f3b 100644 --- a/freqtrade/freqai/data_kitchen.py +++ b/freqtrade/freqai/data_kitchen.py @@ -461,6 +461,20 @@ class FreqaiDataKitchen: return df + def remove_training_from_backtesting( + self + ) -> DataFrame: + """ + Function which takes the backtesting time range and + remove training data from dataframe + """ + tr = self.config["timerange"] + backtesting_timerange = TimeRange.parse_timerange(tr) + start = datetime.fromtimestamp(backtesting_timerange.startts, tz=timezone.utc) + df = self.return_dataframe + df = df.loc[df["date"] >= start, :] + return df + def principal_component_analysis(self) -> None: """ Performs Principal Component Analysis on the data for dimensionality reduction @@ -954,6 +968,7 @@ class FreqaiDataKitchen: to_keep = [col for col in dataframe.columns if not col.startswith("&")] self.return_dataframe = pd.concat([dataframe[to_keep], self.full_df], axis=1) + self.return_dataframe = self.remove_training_from_backtesting() self.full_df = DataFrame() return From 1b6410d7d1e20d4ede790ac46d4232d8cddaa181 Mon Sep 17 00:00:00 2001 From: Emre Date: Thu, 8 Sep 2022 14:12:19 +0300 Subject: [PATCH 16/33] Add XGBoostRegressor for freqAI, fix mypy errors --- .../prediction_models/BaseClassifierModel.py | 8 ++-- .../prediction_models/BaseRegressionModel.py | 8 ++-- .../prediction_models/BaseTensorFlowModel.py | 2 +- .../prediction_models/XGBoostRegressor.py | 46 +++++++++++++++++++ requirements-freqai.txt | 1 + tests/freqai/test_freqai_interface.py | 31 +++++++++++++ 6 files changed, 87 insertions(+), 9 deletions(-) create mode 100644 freqtrade/freqai/prediction_models/XGBoostRegressor.py diff --git a/freqtrade/freqai/prediction_models/BaseClassifierModel.py b/freqtrade/freqai/prediction_models/BaseClassifierModel.py index e51e26e0f..291bacc82 100644 --- a/freqtrade/freqai/prediction_models/BaseClassifierModel.py +++ b/freqtrade/freqai/prediction_models/BaseClassifierModel.py @@ -21,7 +21,7 @@ class BaseClassifierModel(IFreqaiModel): """ def train( - self, unfiltered_dataframe: DataFrame, pair: str, dk: FreqaiDataKitchen + self, unfiltered_dataframe: DataFrame, pair: str, dk: FreqaiDataKitchen, **kwargs ) -> Any: """ Filter the training data and train a model to it. Train makes heavy use of the datakitchen @@ -68,7 +68,7 @@ class BaseClassifierModel(IFreqaiModel): return model def predict( - self, unfiltered_dataframe: DataFrame, dk: FreqaiDataKitchen, first: bool = False + self, dataframe: DataFrame, dk: FreqaiDataKitchen, first: bool = False, **kwargs ) -> Tuple[DataFrame, npt.NDArray[np.int_]]: """ Filter the prediction features data and predict with it. @@ -79,9 +79,9 @@ class BaseClassifierModel(IFreqaiModel): data (NaNs) or felt uncertain about data (PCA and DI index) """ - dk.find_features(unfiltered_dataframe) + dk.find_features(dataframe) filtered_dataframe, _ = dk.filter_features( - unfiltered_dataframe, dk.training_features_list, training_filter=False + dataframe, dk.training_features_list, training_filter=False ) filtered_dataframe = dk.normalize_data_from_metadata(filtered_dataframe) dk.data_dictionary["prediction_features"] = filtered_dataframe diff --git a/freqtrade/freqai/prediction_models/BaseRegressionModel.py b/freqtrade/freqai/prediction_models/BaseRegressionModel.py index 45f0c2937..da6fba571 100644 --- a/freqtrade/freqai/prediction_models/BaseRegressionModel.py +++ b/freqtrade/freqai/prediction_models/BaseRegressionModel.py @@ -20,7 +20,7 @@ class BaseRegressionModel(IFreqaiModel): """ def train( - self, unfiltered_dataframe: DataFrame, pair: str, dk: FreqaiDataKitchen + self, unfiltered_dataframe: DataFrame, pair: str, dk: FreqaiDataKitchen, **kwargs ) -> Any: """ Filter the training data and train a model to it. Train makes heavy use of the datakitchen @@ -67,7 +67,7 @@ class BaseRegressionModel(IFreqaiModel): return model def predict( - self, unfiltered_dataframe: DataFrame, dk: FreqaiDataKitchen, first: bool = False + self, dataframe: DataFrame, dk: FreqaiDataKitchen, first: bool = False, **kwargs ) -> Tuple[DataFrame, npt.NDArray[np.int_]]: """ Filter the prediction features data and predict with it. @@ -78,9 +78,9 @@ class BaseRegressionModel(IFreqaiModel): data (NaNs) or felt uncertain about data (PCA and DI index) """ - dk.find_features(unfiltered_dataframe) + dk.find_features(dataframe) filtered_dataframe, _ = dk.filter_features( - unfiltered_dataframe, dk.training_features_list, training_filter=False + dataframe, dk.training_features_list, training_filter=False ) filtered_dataframe = dk.normalize_data_from_metadata(filtered_dataframe) dk.data_dictionary["prediction_features"] = filtered_dataframe diff --git a/freqtrade/freqai/prediction_models/BaseTensorFlowModel.py b/freqtrade/freqai/prediction_models/BaseTensorFlowModel.py index 66e6ec1fc..6fb49239b 100644 --- a/freqtrade/freqai/prediction_models/BaseTensorFlowModel.py +++ b/freqtrade/freqai/prediction_models/BaseTensorFlowModel.py @@ -17,7 +17,7 @@ class BaseTensorFlowModel(IFreqaiModel): """ def train( - self, unfiltered_dataframe: DataFrame, pair: str, dk: FreqaiDataKitchen + self, unfiltered_dataframe: DataFrame, pair: str, dk: FreqaiDataKitchen, **kwargs ) -> Any: """ Filter the training data and train a model to it. Train makes heavy use of the datakitchen diff --git a/freqtrade/freqai/prediction_models/XGBoostRegressor.py b/freqtrade/freqai/prediction_models/XGBoostRegressor.py new file mode 100644 index 000000000..a8f250d16 --- /dev/null +++ b/freqtrade/freqai/prediction_models/XGBoostRegressor.py @@ -0,0 +1,46 @@ +import logging +from typing import Any, Dict + +import xgboost as xgb + +from freqtrade.freqai.data_kitchen import FreqaiDataKitchen +from freqtrade.freqai.prediction_models.BaseRegressionModel import BaseRegressionModel + + +logger = logging.getLogger(__name__) + + +class XGBoostRegressor(BaseRegressionModel): + """ + User created prediction model. The class needs to override three necessary + functions, predict(), train(), fit(). The class inherits ModelHandler which + has its own DataHandler where data is held, saved, loaded, and managed. + """ + + def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: + """ + User sets up the training and test data to fit their desired model here + :param data_dictionary: the dictionary constructed by DataHandler to hold + all the training and test data/labels. + """ + + xgb.set_config(verbosity=2) + xgb.config_context(verbosity=2) + + X = data_dictionary["train_features"] + y = data_dictionary["train_labels"] + + if self.freqai_info.get("data_split_parameters", {}).get("test_size", 0.1) == 0: + eval_set = None + else: + eval_set = [(data_dictionary["test_features"], data_dictionary["test_labels"])] + + sample_weight = data_dictionary["train_weights"] + + xgb_model = self.get_init_model(dk.pair) + + model = xgb.XGBRegressor(**self.model_training_parameters) + + model.fit(X=X, y=y, sample_weight=sample_weight, eval_set=eval_set, xgb_model=xgb_model) + + return model diff --git a/requirements-freqai.txt b/requirements-freqai.txt index 26e4617af..e8d950382 100644 --- a/requirements-freqai.txt +++ b/requirements-freqai.txt @@ -6,3 +6,4 @@ scikit-learn==1.1.2 joblib==1.1.0 catboost==1.0.6; platform_machine != 'aarch64' lightgbm==3.3.2 +xgboost==1.6.2 diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index 5441b3c24..7783c00e7 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -172,6 +172,37 @@ def test_train_model_in_series_LightGBMClassifier(mocker, freqai_conf): shutil.rmtree(Path(freqai.dk.full_path)) +def test_train_model_in_series_XGBoostRegressor(mocker, freqai_conf): + freqai_conf.update({"timerange": "20180110-20180130"}) + freqai_conf.update({"freqaimodel": "XGBoostRegressor"}) + freqai_conf.update({"strategy": "freqai_test_strat"}) + + strategy = get_patched_freqai_strategy(mocker, freqai_conf) + exchange = get_patched_exchange(mocker, freqai_conf) + strategy.dp = DataProvider(freqai_conf, exchange) + strategy.freqai_info = freqai_conf.get("freqai", {}) + freqai = strategy.freqai + freqai.live = True + freqai.dk = FreqaiDataKitchen(freqai_conf) + timerange = TimeRange.parse_timerange("20180110-20180130") + freqai.dd.load_all_pair_histories(timerange, freqai.dk) + + freqai.dd.pair_dict = MagicMock() + + data_load_timerange = TimeRange.parse_timerange("20180110-20180130") + new_timerange = TimeRange.parse_timerange("20180120-20180130") + + freqai.train_model_in_series(new_timerange, "ADA/BTC", + strategy, freqai.dk, data_load_timerange) + + assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_model.joblib").is_file() + assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_metadata.json").is_file() + assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_trained_df.pkl").is_file() + assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_svm_model.joblib").is_file() + + shutil.rmtree(Path(freqai.dk.full_path)) + + def test_start_backtesting(mocker, freqai_conf): freqai_conf.update({"timerange": "20180120-20180130"}) freqai_conf.get("freqai", {}).update({"save_backtest_models": True}) From df6e43d2c599badef3247f8878cacec2a81f5f79 Mon Sep 17 00:00:00 2001 From: Emre Date: Fri, 9 Sep 2022 00:11:09 +0300 Subject: [PATCH 17/33] Add XGBoostRegressorMultiTarget class --- .../XGBoostRegressorMultiTarget.py | 43 +++++++++++++++++++ tests/freqai/test_freqai_interface.py | 31 +++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 freqtrade/freqai/prediction_models/XGBoostRegressorMultiTarget.py diff --git a/freqtrade/freqai/prediction_models/XGBoostRegressorMultiTarget.py b/freqtrade/freqai/prediction_models/XGBoostRegressorMultiTarget.py new file mode 100644 index 000000000..5283501d1 --- /dev/null +++ b/freqtrade/freqai/prediction_models/XGBoostRegressorMultiTarget.py @@ -0,0 +1,43 @@ +import logging +from typing import Any, Dict + +from sklearn.multioutput import MultiOutputRegressor +from xgboost import XGBRegressor + +from freqtrade.freqai.data_kitchen import FreqaiDataKitchen +from freqtrade.freqai.prediction_models.BaseRegressionModel import BaseRegressionModel + + +logger = logging.getLogger(__name__) + + +class XGBoostRegressorMultiTarget(BaseRegressionModel): + """ + User created prediction model. The class needs to override three necessary + functions, predict(), train(), fit(). The class inherits ModelHandler which + has its own DataHandler where data is held, saved, loaded, and managed. + """ + + def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: + """ + User sets up the training and test data to fit their desired model here + :param data_dictionary: the dictionary constructed by DataHandler to hold + all the training and test data/labels. + """ + + xgb = XGBRegressor(**self.model_training_parameters) + + X = data_dictionary["train_features"] + y = data_dictionary["train_labels"] + eval_set = (data_dictionary["test_features"], data_dictionary["test_labels"]) + sample_weight = data_dictionary["train_weights"] + + if self.continual_learning: + logger.warning('Continual learning not supported for MultiTarget models') + + model = MultiOutputRegressor(estimator=xgb) + model.fit(X=X, y=y, sample_weight=sample_weight) # , eval_set=eval_set) + train_score = model.score(X, y) + test_score = model.score(*eval_set) + logger.info(f"Train score {train_score}, Test score {test_score}") + return model diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index 7783c00e7..ff0eb24a9 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -203,6 +203,37 @@ def test_train_model_in_series_XGBoostRegressor(mocker, freqai_conf): shutil.rmtree(Path(freqai.dk.full_path)) +def test_train_model_in_series_XGBoostRegressorMultiModel(mocker, freqai_conf): + freqai_conf.update({"timerange": "20180110-20180130"}) + freqai_conf.update({"freqaimodel": "XGBoostRegressorMultiTarget"}) + freqai_conf.update({"strategy": "freqai_test_multimodel_strat"}) + strategy = get_patched_freqai_strategy(mocker, freqai_conf) + exchange = get_patched_exchange(mocker, freqai_conf) + strategy.dp = DataProvider(freqai_conf, exchange) + strategy.freqai_info = freqai_conf.get("freqai", {}) + freqai = strategy.freqai + freqai.live = True + freqai.dk = FreqaiDataKitchen(freqai_conf) + timerange = TimeRange.parse_timerange("20180110-20180130") + freqai.dd.load_all_pair_histories(timerange, freqai.dk) + + freqai.dd.pair_dict = MagicMock() + + data_load_timerange = TimeRange.parse_timerange("20180110-20180130") + new_timerange = TimeRange.parse_timerange("20180120-20180130") + + freqai.train_model_in_series(new_timerange, "ADA/BTC", strategy, freqai.dk, data_load_timerange) + + assert len(freqai.dk.label_list) == 2 + assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_model.joblib").is_file() + assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_metadata.json").is_file() + assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_trained_df.pkl").is_file() + assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_svm_model.joblib").is_file() + assert len(freqai.dk.data['training_features_list']) == 26 + + shutil.rmtree(Path(freqai.dk.full_path)) + + def test_start_backtesting(mocker, freqai_conf): freqai_conf.update({"timerange": "20180120-20180130"}) freqai_conf.get("freqai", {}).update({"save_backtest_models": True}) From acb410a0defb4626b52879b56e3d4ab8824c1085 Mon Sep 17 00:00:00 2001 From: Emre Date: Fri, 9 Sep 2022 00:11:43 +0300 Subject: [PATCH 18/33] Remove verbosity params --- freqtrade/freqai/prediction_models/XGBoostRegressor.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/freqtrade/freqai/prediction_models/XGBoostRegressor.py b/freqtrade/freqai/prediction_models/XGBoostRegressor.py index a8f250d16..acc4386f5 100644 --- a/freqtrade/freqai/prediction_models/XGBoostRegressor.py +++ b/freqtrade/freqai/prediction_models/XGBoostRegressor.py @@ -1,7 +1,7 @@ import logging from typing import Any, Dict -import xgboost as xgb +from xgboost import XGBRegressor from freqtrade.freqai.data_kitchen import FreqaiDataKitchen from freqtrade.freqai.prediction_models.BaseRegressionModel import BaseRegressionModel @@ -24,9 +24,6 @@ class XGBoostRegressor(BaseRegressionModel): all the training and test data/labels. """ - xgb.set_config(verbosity=2) - xgb.config_context(verbosity=2) - X = data_dictionary["train_features"] y = data_dictionary["train_labels"] @@ -39,7 +36,7 @@ class XGBoostRegressor(BaseRegressionModel): xgb_model = self.get_init_model(dk.pair) - model = xgb.XGBRegressor(**self.model_training_parameters) + model = XGBRegressor(**self.model_training_parameters) model.fit(X=X, y=y, sample_weight=sample_weight, eval_set=eval_set, xgb_model=xgb_model) From a826c0eb837af2f4f4b68660481e5c9f436caac7 Mon Sep 17 00:00:00 2001 From: robcaulk Date: Fri, 9 Sep 2022 19:17:15 +0200 Subject: [PATCH 19/33] ensure signatures match, reduce verbosity --- freqtrade/freqai/freqai_interface.py | 8 +++--- .../prediction_models/BaseClassifierModel.py | 26 +++++++++---------- .../prediction_models/BaseRegressionModel.py | 26 +++++++++---------- .../prediction_models/BaseTensorFlowModel.py | 10 +++---- 4 files changed, 35 insertions(+), 35 deletions(-) diff --git a/freqtrade/freqai/freqai_interface.py b/freqtrade/freqai/freqai_interface.py index 101df88ec..e4f77a9cf 100644 --- a/freqtrade/freqai/freqai_interface.py +++ b/freqtrade/freqai/freqai_interface.py @@ -673,12 +673,12 @@ class IFreqaiModel(ABC): # See freqai/prediction_models/CatboostPredictionModel.py for an example. @abstractmethod - def train(self, unfiltered_dataframe: DataFrame, pair: str, + def train(self, unfiltered_df: DataFrame, pair: str, dk: FreqaiDataKitchen, **kwargs) -> Any: """ Filter the training data and train a model to it. Train makes heavy use of the datahandler for storing, saving, loading, and analyzing the data. - :param unfiltered_dataframe: Full dataframe for the current training period + :param unfiltered_df: Full dataframe for the current training period :param metadata: pair metadata from strategy. :return: Trained model which can be used to inference (self.predict) """ @@ -697,11 +697,11 @@ class IFreqaiModel(ABC): @abstractmethod def predict( - self, dataframe: DataFrame, dk: FreqaiDataKitchen, first: bool = True, **kwargs + self, unfiltered_df: DataFrame, dk: FreqaiDataKitchen, **kwargs ) -> Tuple[DataFrame, NDArray[np.int_]]: """ Filter the prediction features data and predict with it. - :param unfiltered_dataframe: Full dataframe for the current backtest period. + :param unfiltered_df: Full dataframe for the current backtest period. :param dk: FreqaiDataKitchen = Data management/analysis tool associated to present pair only :param first: boolean = whether this is the first prediction or not. :return: diff --git a/freqtrade/freqai/prediction_models/BaseClassifierModel.py b/freqtrade/freqai/prediction_models/BaseClassifierModel.py index 291bacc82..5142ffb0d 100644 --- a/freqtrade/freqai/prediction_models/BaseClassifierModel.py +++ b/freqtrade/freqai/prediction_models/BaseClassifierModel.py @@ -21,12 +21,12 @@ class BaseClassifierModel(IFreqaiModel): """ def train( - self, unfiltered_dataframe: DataFrame, pair: str, dk: FreqaiDataKitchen, **kwargs + self, unfiltered_df: DataFrame, pair: str, dk: FreqaiDataKitchen, **kwargs ) -> Any: """ Filter the training data and train a model to it. Train makes heavy use of the datakitchen for storing, saving, loading, and analyzing the data. - :param unfiltered_dataframe: Full dataframe for the current training period + :param unfiltered_df: Full dataframe for the current training period :param metadata: pair metadata from strategy. :return: :model: Trained model which can be used to inference (self.predict) @@ -36,14 +36,14 @@ class BaseClassifierModel(IFreqaiModel): # filter the features requested by user in the configuration file and elegantly handle NaNs features_filtered, labels_filtered = dk.filter_features( - unfiltered_dataframe, + unfiltered_df, dk.training_features_list, dk.label_list, training_filter=True, ) - start_date = unfiltered_dataframe["date"].iloc[0].strftime("%Y-%m-%d") - end_date = unfiltered_dataframe["date"].iloc[-1].strftime("%Y-%m-%d") + start_date = unfiltered_df["date"].iloc[0].strftime("%Y-%m-%d") + end_date = unfiltered_df["date"].iloc[-1].strftime("%Y-%m-%d") logger.info(f"-------------------- Training on data from {start_date} to " f"{end_date}--------------------") # split data into train/test data. @@ -68,25 +68,25 @@ class BaseClassifierModel(IFreqaiModel): return model def predict( - self, dataframe: DataFrame, dk: FreqaiDataKitchen, first: bool = False, **kwargs + self, unfiltered_df: DataFrame, dk: FreqaiDataKitchen, **kwargs ) -> Tuple[DataFrame, npt.NDArray[np.int_]]: """ Filter the prediction features data and predict with it. - :param: unfiltered_dataframe: Full dataframe for the current backtest period. + :param: unfiltered_df: Full dataframe for the current backtest period. :return: :pred_df: dataframe containing the predictions :do_predict: np.array of 1s and 0s to indicate places where freqai needed to remove data (NaNs) or felt uncertain about data (PCA and DI index) """ - dk.find_features(dataframe) - filtered_dataframe, _ = dk.filter_features( - dataframe, dk.training_features_list, training_filter=False + dk.find_features(unfiltered_df) + filtered_df, _ = dk.filter_features( + unfiltered_df, dk.training_features_list, training_filter=False ) - filtered_dataframe = dk.normalize_data_from_metadata(filtered_dataframe) - dk.data_dictionary["prediction_features"] = filtered_dataframe + filtered_df = dk.normalize_data_from_metadata(filtered_df) + dk.data_dictionary["prediction_features"] = filtered_df - self.data_cleaning_predict(dk, filtered_dataframe) + self.data_cleaning_predict(dk, filtered_df) predictions = self.model.predict(dk.data_dictionary["prediction_features"]) pred_df = DataFrame(predictions, columns=dk.label_list) diff --git a/freqtrade/freqai/prediction_models/BaseRegressionModel.py b/freqtrade/freqai/prediction_models/BaseRegressionModel.py index da6fba571..1d87e42c0 100644 --- a/freqtrade/freqai/prediction_models/BaseRegressionModel.py +++ b/freqtrade/freqai/prediction_models/BaseRegressionModel.py @@ -20,12 +20,12 @@ class BaseRegressionModel(IFreqaiModel): """ def train( - self, unfiltered_dataframe: DataFrame, pair: str, dk: FreqaiDataKitchen, **kwargs + self, unfiltered_df: DataFrame, pair: str, dk: FreqaiDataKitchen, **kwargs ) -> Any: """ Filter the training data and train a model to it. Train makes heavy use of the datakitchen for storing, saving, loading, and analyzing the data. - :param unfiltered_dataframe: Full dataframe for the current training period + :param unfiltered_df: Full dataframe for the current training period :param metadata: pair metadata from strategy. :return: :model: Trained model which can be used to inference (self.predict) @@ -35,14 +35,14 @@ class BaseRegressionModel(IFreqaiModel): # filter the features requested by user in the configuration file and elegantly handle NaNs features_filtered, labels_filtered = dk.filter_features( - unfiltered_dataframe, + unfiltered_df, dk.training_features_list, dk.label_list, training_filter=True, ) - start_date = unfiltered_dataframe["date"].iloc[0].strftime("%Y-%m-%d") - end_date = unfiltered_dataframe["date"].iloc[-1].strftime("%Y-%m-%d") + start_date = unfiltered_df["date"].iloc[0].strftime("%Y-%m-%d") + end_date = unfiltered_df["date"].iloc[-1].strftime("%Y-%m-%d") logger.info(f"-------------------- Training on data from {start_date} to " f"{end_date}--------------------") # split data into train/test data. @@ -67,26 +67,26 @@ class BaseRegressionModel(IFreqaiModel): return model def predict( - self, dataframe: DataFrame, dk: FreqaiDataKitchen, first: bool = False, **kwargs + self, unfiltered_df: DataFrame, dk: FreqaiDataKitchen, **kwargs ) -> Tuple[DataFrame, npt.NDArray[np.int_]]: """ Filter the prediction features data and predict with it. - :param: unfiltered_dataframe: Full dataframe for the current backtest period. + :param: unfiltered_df: Full dataframe for the current backtest period. :return: :pred_df: dataframe containing the predictions :do_predict: np.array of 1s and 0s to indicate places where freqai needed to remove data (NaNs) or felt uncertain about data (PCA and DI index) """ - dk.find_features(dataframe) - filtered_dataframe, _ = dk.filter_features( - dataframe, dk.training_features_list, training_filter=False + dk.find_features(unfiltered_df) + filtered_df, _ = dk.filter_features( + unfiltered_df, dk.training_features_list, training_filter=False ) - filtered_dataframe = dk.normalize_data_from_metadata(filtered_dataframe) - dk.data_dictionary["prediction_features"] = filtered_dataframe + filtered_df = dk.normalize_data_from_metadata(filtered_df) + dk.data_dictionary["prediction_features"] = filtered_df # optional additional data cleaning/analysis - self.data_cleaning_predict(dk, filtered_dataframe) + self.data_cleaning_predict(dk, filtered_df) predictions = self.model.predict(dk.data_dictionary["prediction_features"]) pred_df = DataFrame(predictions, columns=dk.label_list) diff --git a/freqtrade/freqai/prediction_models/BaseTensorFlowModel.py b/freqtrade/freqai/prediction_models/BaseTensorFlowModel.py index 6fb49239b..eea80f3a2 100644 --- a/freqtrade/freqai/prediction_models/BaseTensorFlowModel.py +++ b/freqtrade/freqai/prediction_models/BaseTensorFlowModel.py @@ -17,12 +17,12 @@ class BaseTensorFlowModel(IFreqaiModel): """ def train( - self, unfiltered_dataframe: DataFrame, pair: str, dk: FreqaiDataKitchen, **kwargs + self, unfiltered_df: DataFrame, pair: str, dk: FreqaiDataKitchen, **kwargs ) -> Any: """ Filter the training data and train a model to it. Train makes heavy use of the datakitchen for storing, saving, loading, and analyzing the data. - :param unfiltered_dataframe: Full dataframe for the current training period + :param unfiltered_df: Full dataframe for the current training period :param metadata: pair metadata from strategy. :return: :model: Trained model which can be used to inference (self.predict) @@ -32,14 +32,14 @@ class BaseTensorFlowModel(IFreqaiModel): # filter the features requested by user in the configuration file and elegantly handle NaNs features_filtered, labels_filtered = dk.filter_features( - unfiltered_dataframe, + unfiltered_df, dk.training_features_list, dk.label_list, training_filter=True, ) - start_date = unfiltered_dataframe["date"].iloc[0].strftime("%Y-%m-%d") - end_date = unfiltered_dataframe["date"].iloc[-1].strftime("%Y-%m-%d") + start_date = unfiltered_df["date"].iloc[0].strftime("%Y-%m-%d") + end_date = unfiltered_df["date"].iloc[-1].strftime("%Y-%m-%d") logger.info(f"-------------------- Training on data from {start_date} to " f"{end_date}--------------------") # split data into train/test data. From c13bec26d1b01a6173396dcf1f4daed9236eaad8 Mon Sep 17 00:00:00 2001 From: robcaulk Date: Fri, 9 Sep 2022 19:41:28 +0200 Subject: [PATCH 20/33] add freqaimodel to hyperopt command --- docs/freqai.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/freqai.md b/docs/freqai.md index 92e019dfe..09a51c713 100644 --- a/docs/freqai.md +++ b/docs/freqai.md @@ -541,7 +541,7 @@ for each pair, for each backtesting window within the expanded `--timerange`. Users can hyperopt using the same command as typical [hyperopt](hyperopt.md): ```bash -freqtrade hyperopt --hyperopt-loss SharpeHyperOptLoss --strategy FreqaiExampleStrategy --strategy-path freqtrade/templates --config config_examples/config_freqai.example.json --timerange 20220428-20220507 +freqtrade hyperopt --hyperopt-loss SharpeHyperOptLoss --strategy FreqaiExampleStrategy --freqaimodel LightGBMRegressor --strategy-path freqtrade/templates --config config_examples/config_freqai.example.json --timerange 20220428-20220507 ``` Users need to have the data pre-downloaded in the same fashion as if they were doing a FreqAI [backtest](#backtesting). In addition, users must consider some restrictions when trying to [Hyperopt](hyperopt.md) FreqAI strategies: From 37fcbeba580143179c9344021ba2b5ceea516902 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 9 Sep 2022 19:58:55 +0200 Subject: [PATCH 21/33] Update backtesting output wording to Entries --- docs/backtesting.md | 74 ++++++++++++------------- freqtrade/optimize/optimize_reports.py | 5 +- tests/optimize/test_optimize_reports.py | 24 ++++---- 3 files changed, 52 insertions(+), 51 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index 8b2fdc345..f20a53d22 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -107,7 +107,7 @@ Strategy arguments: ## Test your strategy with Backtesting -Now you have good Buy and Sell strategies and some historic data, you want to test it against +Now you have good Entry and exit strategies and some historic data, you want to test it against real data. This is what we call [backtesting](https://en.wikipedia.org/wiki/Backtesting). Backtesting will use the crypto-currencies (pairs) from your config file and load historical candle (OHLCV) data from `user_data/data/` by default. @@ -215,7 +215,7 @@ Sometimes your account has certain fee rebates (fee reductions starting with a c To account for this in backtesting, you can use the `--fee` command line option to supply this value to backtesting. This fee must be a ratio, and will be applied twice (once for trade entry, and once for trade exit). -For example, if the buying and selling commission fee is 0.1% (i.e., 0.001 written as ratio), then you would run backtesting as the following: +For example, if the commission fee per order is 0.1% (i.e., 0.001 written as ratio), then you would run backtesting as the following: ```bash freqtrade backtesting --fee 0.001 @@ -252,41 +252,41 @@ The most important in the backtesting is to understand the result. A backtesting result will look like that: ``` -========================================================= BACKTESTING REPORT ========================================================== -| Pair | Buys | Avg Profit % | Cum Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Wins Draws Loss Win% | -|:---------|-------:|---------------:|---------------:|-----------------:|---------------:|:-------------|-------------------------:| -| ADA/BTC | 35 | -0.11 | -3.88 | -0.00019428 | -1.94 | 4:35:00 | 14 0 21 40.0 | -| ARK/BTC | 11 | -0.41 | -4.52 | -0.00022647 | -2.26 | 2:03:00 | 3 0 8 27.3 | -| BTS/BTC | 32 | 0.31 | 9.78 | 0.00048938 | 4.89 | 5:05:00 | 18 0 14 56.2 | -| DASH/BTC | 13 | -0.08 | -1.07 | -0.00005343 | -0.53 | 4:39:00 | 6 0 7 46.2 | -| ENG/BTC | 18 | 1.36 | 24.54 | 0.00122807 | 12.27 | 2:50:00 | 8 0 10 44.4 | -| EOS/BTC | 36 | 0.08 | 3.06 | 0.00015304 | 1.53 | 3:34:00 | 16 0 20 44.4 | -| ETC/BTC | 26 | 0.37 | 9.51 | 0.00047576 | 4.75 | 6:14:00 | 11 0 15 42.3 | -| ETH/BTC | 33 | 0.30 | 9.96 | 0.00049856 | 4.98 | 7:31:00 | 16 0 17 48.5 | -| IOTA/BTC | 32 | 0.03 | 1.09 | 0.00005444 | 0.54 | 3:12:00 | 14 0 18 43.8 | -| LSK/BTC | 15 | 1.75 | 26.26 | 0.00131413 | 13.13 | 2:58:00 | 6 0 9 40.0 | -| LTC/BTC | 32 | -0.04 | -1.38 | -0.00006886 | -0.69 | 4:49:00 | 11 0 21 34.4 | -| NANO/BTC | 17 | 1.26 | 21.39 | 0.00107058 | 10.70 | 1:55:00 | 10 0 7 58.5 | -| NEO/BTC | 23 | 0.82 | 18.97 | 0.00094936 | 9.48 | 2:59:00 | 10 0 13 43.5 | -| REQ/BTC | 9 | 1.17 | 10.54 | 0.00052734 | 5.27 | 3:47:00 | 4 0 5 44.4 | -| XLM/BTC | 16 | 1.22 | 19.54 | 0.00097800 | 9.77 | 3:15:00 | 7 0 9 43.8 | -| XMR/BTC | 23 | -0.18 | -4.13 | -0.00020696 | -2.07 | 5:30:00 | 12 0 11 52.2 | -| XRP/BTC | 35 | 0.66 | 22.96 | 0.00114897 | 11.48 | 3:49:00 | 12 0 23 34.3 | -| ZEC/BTC | 22 | -0.46 | -10.18 | -0.00050971 | -5.09 | 2:22:00 | 7 0 15 31.8 | -| TOTAL | 429 | 0.36 | 152.41 | 0.00762792 | 76.20 | 4:12:00 | 186 0 243 43.4 | +========================================================= BACKTESTING REPORT ========================================================= +| Pair | Entries | Avg Profit % | Cum Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Wins Draws Loss Win% | +|:---------|--------:|---------------:|---------------:|-----------------:|---------------:|:-------------|-------------------------:| +| ADA/BTC | 35 | -0.11 | -3.88 | -0.00019428 | -1.94 | 4:35:00 | 14 0 21 40.0 | +| ARK/BTC | 11 | -0.41 | -4.52 | -0.00022647 | -2.26 | 2:03:00 | 3 0 8 27.3 | +| BTS/BTC | 32 | 0.31 | 9.78 | 0.00048938 | 4.89 | 5:05:00 | 18 0 14 56.2 | +| DASH/BTC | 13 | -0.08 | -1.07 | -0.00005343 | -0.53 | 4:39:00 | 6 0 7 46.2 | +| ENG/BTC | 18 | 1.36 | 24.54 | 0.00122807 | 12.27 | 2:50:00 | 8 0 10 44.4 | +| EOS/BTC | 36 | 0.08 | 3.06 | 0.00015304 | 1.53 | 3:34:00 | 16 0 20 44.4 | +| ETC/BTC | 26 | 0.37 | 9.51 | 0.00047576 | 4.75 | 6:14:00 | 11 0 15 42.3 | +| ETH/BTC | 33 | 0.30 | 9.96 | 0.00049856 | 4.98 | 7:31:00 | 16 0 17 48.5 | +| IOTA/BTC | 32 | 0.03 | 1.09 | 0.00005444 | 0.54 | 3:12:00 | 14 0 18 43.8 | +| LSK/BTC | 15 | 1.75 | 26.26 | 0.00131413 | 13.13 | 2:58:00 | 6 0 9 40.0 | +| LTC/BTC | 32 | -0.04 | -1.38 | -0.00006886 | -0.69 | 4:49:00 | 11 0 21 34.4 | +| NANO/BTC | 17 | 1.26 | 21.39 | 0.00107058 | 10.70 | 1:55:00 | 10 0 7 58.5 | +| NEO/BTC | 23 | 0.82 | 18.97 | 0.00094936 | 9.48 | 2:59:00 | 10 0 13 43.5 | +| REQ/BTC | 9 | 1.17 | 10.54 | 0.00052734 | 5.27 | 3:47:00 | 4 0 5 44.4 | +| XLM/BTC | 16 | 1.22 | 19.54 | 0.00097800 | 9.77 | 3:15:00 | 7 0 9 43.8 | +| XMR/BTC | 23 | -0.18 | -4.13 | -0.00020696 | -2.07 | 5:30:00 | 12 0 11 52.2 | +| XRP/BTC | 35 | 0.66 | 22.96 | 0.00114897 | 11.48 | 3:49:00 | 12 0 23 34.3 | +| ZEC/BTC | 22 | -0.46 | -10.18 | -0.00050971 | -5.09 | 2:22:00 | 7 0 15 31.8 | +| TOTAL | 429 | 0.36 | 152.41 | 0.00762792 | 76.20 | 4:12:00 | 186 0 243 43.4 | ========================================================= EXIT REASON STATS ========================================================== -| Exit Reason | Sells | Wins | Draws | Losses | +| Exit Reason | Exits | Wins | Draws | Losses | |:-------------------|--------:|------:|-------:|--------:| | trailing_stop_loss | 205 | 150 | 0 | 55 | | stop_loss | 166 | 0 | 0 | 166 | | exit_signal | 56 | 36 | 0 | 20 | | force_exit | 2 | 0 | 0 | 2 | ====================================================== LEFT OPEN TRADES REPORT ====================================================== -| Pair | Buys | Avg Profit % | Cum Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Win Draw Loss Win% | -|:---------|-------:|---------------:|---------------:|-----------------:|---------------:|:---------------|--------------------:| -| ADA/BTC | 1 | 0.89 | 0.89 | 0.00004434 | 0.44 | 6:00:00 | 1 0 0 100 | -| LTC/BTC | 1 | 0.68 | 0.68 | 0.00003421 | 0.34 | 2:00:00 | 1 0 0 100 | -| TOTAL | 2 | 0.78 | 1.57 | 0.00007855 | 0.78 | 4:00:00 | 2 0 0 100 | +| Pair | Entries | Avg Profit % | Cum Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Win Draw Loss Win% | +|:---------|---------:|---------------:|---------------:|-----------------:|---------------:|:---------------|--------------------:| +| ADA/BTC | 1 | 0.89 | 0.89 | 0.00004434 | 0.44 | 6:00:00 | 1 0 0 100 | +| LTC/BTC | 1 | 0.68 | 0.68 | 0.00003421 | 0.34 | 2:00:00 | 1 0 0 100 | +| TOTAL | 2 | 0.78 | 1.57 | 0.00007855 | 0.78 | 4:00:00 | 2 0 0 100 | ================== SUMMARY METRICS ================== | Metric | Value | |-----------------------------+---------------------| @@ -356,7 +356,7 @@ The column `Avg Profit %` shows the average profit for all trades made while the The column `Tot Profit %` shows instead the total profit % in relation to the starting balance. In the above results, we have a starting balance of 0.01 BTC and an absolute profit of 0.00762792 BTC - so the `Tot Profit %` will be `(0.00762792 / 0.01) * 100 ~= 76.2%`. -Your strategy performance is influenced by your buy strategy, your exit strategy, and also by the `minimal_roi` and `stop_loss` you have set. +Your strategy performance is influenced by your entry strategy, your exit strategy, and also by the `minimal_roi` and `stop_loss` you have set. For example, if your `minimal_roi` is only `"0": 0.01` you cannot expect the bot to make more profit than 1% (because it will exit every time a trade reaches 1%). @@ -515,7 +515,7 @@ You can then load the trades to perform further analysis as shown in the [data a Since backtesting lacks some detailed information about what happens within a candle, it needs to take a few assumptions: - Exchange [trading limits](#trading-limits-in-backtesting) are respected -- Buys happen at open-price +- Entries happen at open-price - All orders are filled at the requested price (no slippage, no unfilled orders) - Exit-signal exits happen at open-price of the consecutive candle - Exit-signal is favored over Stoploss, because exit-signals are assumed to trigger on candle's open @@ -612,11 +612,11 @@ There will be an additional table comparing win/losses of the different strategi Detailed output for all strategies one after the other will be available, so make sure to scroll up to see the details per strategy. ``` -=========================================================== STRATEGY SUMMARY ========================================================================= -| Strategy | Buys | Avg Profit % | Cum Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Wins | Draws | Losses | Drawdown % | -|:------------|-------:|---------------:|---------------:|-----------------:|---------------:|:---------------|------:|-------:|-------:|-----------:| -| Strategy1 | 429 | 0.36 | 152.41 | 0.00762792 | 76.20 | 4:12:00 | 186 | 0 | 243 | 45.2 | -| Strategy2 | 1487 | -0.13 | -197.58 | -0.00988917 | -98.79 | 4:43:00 | 662 | 0 | 825 | 241.68 | +=========================================================== STRATEGY SUMMARY =========================================================================== +| Strategy | Entries | Avg Profit % | Cum Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Wins | Draws | Losses | Drawdown % | +|:------------|---------:|---------------:|---------------:|-----------------:|---------------:|:---------------|------:|-------:|-------:|-----------:| +| Strategy1 | 429 | 0.36 | 152.41 | 0.00762792 | 76.20 | 4:12:00 | 186 | 0 | 243 | 45.2 | +| Strategy2 | 1487 | -0.13 | -197.58 | -0.00988917 | -98.79 | 4:43:00 | 662 | 0 | 825 | 241.68 | ``` ## Next step diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 519022db2..fa6c3f161 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -75,7 +75,8 @@ def _get_line_floatfmt(stake_currency: str) -> List[str]: '.2f', 'd', 's', 's'] -def _get_line_header(first_column: str, stake_currency: str, direction: str = 'Buys') -> List[str]: +def _get_line_header(first_column: str, stake_currency: str, + direction: str = 'Entries') -> List[str]: """ Generate header lines (goes in line with _generate_result_line()) """ @@ -642,7 +643,7 @@ def text_table_tags(tag_type: str, tag_results: List[Dict[str, Any]], stake_curr if (tag_type == "enter_tag"): headers = _get_line_header("TAG", stake_currency) else: - headers = _get_line_header("TAG", stake_currency, 'Sells') + headers = _get_line_header("TAG", stake_currency, 'Exits') floatfmt = _get_line_floatfmt(stake_currency) output = [ [ diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index 562e12820..5095f2fde 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -40,14 +40,14 @@ def test_text_table_bt_results(): ) result_str = ( - '| Pair | Buys | Avg Profit % | Cum Profit % | Tot Profit BTC | Tot Profit % |' - ' Avg Duration | Win Draw Loss Win% |\n' - '|---------+--------+----------------+----------------+------------------+----------------+' - '----------------+-------------------------|\n' - '| ETH/BTC | 3 | 8.33 | 25.00 | 0.50000000 | 12.50 |' - ' 0:20:00 | 2 0 1 66.7 |\n' - '| TOTAL | 3 | 8.33 | 25.00 | 0.50000000 | 12.50 |' - ' 0:20:00 | 2 0 1 66.7 |' + '| Pair | Entries | Avg Profit % | Cum Profit % | Tot Profit BTC | ' + 'Tot Profit % | Avg Duration | Win Draw Loss Win% |\n' + '|---------+-----------+----------------+----------------+------------------+' + '----------------+----------------+-------------------------|\n' + '| ETH/BTC | 3 | 8.33 | 25.00 | 0.50000000 | ' + '12.50 | 0:20:00 | 2 0 1 66.7 |\n' + '| TOTAL | 3 | 8.33 | 25.00 | 0.50000000 | ' + '12.50 | 0:20:00 | 2 0 1 66.7 |' ) pair_results = generate_pair_metrics(['ETH/BTC'], stake_currency='BTC', @@ -402,13 +402,13 @@ def test_text_table_strategy(testdatadir): bt_res_data_comparison = bt_res_data.pop('strategy_comparison') result_str = ( - '| Strategy | Buys | Avg Profit % | Cum Profit % | Tot Profit BTC |' + '| Strategy | Entries | Avg Profit % | Cum Profit % | Tot Profit BTC |' ' Tot Profit % | Avg Duration | Win Draw Loss Win% | Drawdown |\n' - '|----------------+--------+----------------+----------------+------------------+' + '|----------------+-----------+----------------+----------------+------------------+' '----------------+----------------+-------------------------+-----------------------|\n' - '| StrategyTestV2 | 179 | 0.08 | 14.39 | 0.02608550 |' + '| StrategyTestV2 | 179 | 0.08 | 14.39 | 0.02608550 |' ' 260.85 | 3:40:00 | 170 0 9 95.0 | 0.00308222 BTC 8.67% |\n' - '| TestStrategy | 179 | 0.08 | 14.39 | 0.02608550 |' + '| TestStrategy | 179 | 0.08 | 14.39 | 0.02608550 |' ' 260.85 | 3:40:00 | 170 0 9 95.0 | 0.00308222 BTC 8.67% |' ) From 97be3318f447445dfaab52f8b4d08ba2f8e59112 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 9 Sep 2022 20:17:34 +0200 Subject: [PATCH 22/33] Use Datetime_format from constants --- freqtrade/data/history/history_utils.py | 8 ++++---- freqtrade/freqai/freqai_interface.py | 5 +++-- tests/data/test_history.py | 6 +++--- tests/test_persistence.py | 10 +++++----- 4 files changed, 15 insertions(+), 14 deletions(-) diff --git a/freqtrade/data/history/history_utils.py b/freqtrade/data/history/history_utils.py index 7a3fa4e0c..6a6e29429 100644 --- a/freqtrade/data/history/history_utils.py +++ b/freqtrade/data/history/history_utils.py @@ -228,9 +228,9 @@ def _download_pair_history(pair: str, *, ) logger.debug("Current Start: %s", - f"{data.iloc[0]['date']:%Y-%m-%d %H:%M:%S}" if not data.empty else 'None') + f"{data.iloc[0]['date']:DATETIME_PRINT_FORMAT}" if not data.empty else 'None') logger.debug("Current End: %s", - f"{data.iloc[-1]['date']:%Y-%m-%d %H:%M:%S}" if not data.empty else 'None') + f"{data.iloc[-1]['date']:DATETIME_PRINT_FORMAT}" if not data.empty else 'None') # Default since_ms to 30 days if nothing is given new_data = exchange.get_historic_ohlcv(pair=pair, @@ -254,9 +254,9 @@ def _download_pair_history(pair: str, *, fill_missing=False, drop_incomplete=False) logger.debug("New Start: %s", - f"{data.iloc[0]['date']:%Y-%m-%d %H:%M:%S}" if not data.empty else 'None') + f"{data.iloc[0]['date']:DATETIME_PRINT_FORMAT}" if not data.empty else 'None') logger.debug("New End: %s", - f"{data.iloc[-1]['date']:%Y-%m-%d %H:%M:%S}" if not data.empty else 'None') + f"{data.iloc[-1]['date']:DATETIME_PRINT_FORMAT}" if not data.empty else 'None') data_handler.ohlcv_store(pair, timeframe, data=data, candle_type=candle_type) return True diff --git a/freqtrade/freqai/freqai_interface.py b/freqtrade/freqai/freqai_interface.py index c5ac17a3a..7a48f6e15 100644 --- a/freqtrade/freqai/freqai_interface.py +++ b/freqtrade/freqai/freqai_interface.py @@ -14,6 +14,7 @@ from numpy.typing import NDArray from pandas import DataFrame from freqtrade.configuration import TimeRange +from freqtrade.constants import DATETIME_PRINT_FORMAT from freqtrade.enums import RunMode from freqtrade.exceptions import OperationalException from freqtrade.exchange import timeframe_to_seconds @@ -232,10 +233,10 @@ class IFreqaiModel(ABC): trained_timestamp = tr_train tr_train_startts_str = datetime.fromtimestamp( tr_train.startts, - tz=timezone.utc).strftime("%Y-%m-%d %H:%M:%S") + tz=timezone.utc).strftime(DATETIME_PRINT_FORMAT) tr_train_stopts_str = datetime.fromtimestamp( tr_train.stopts, - tz=timezone.utc).strftime("%Y-%m-%d %H:%M:%S") + tz=timezone.utc).strftime(DATETIME_PRINT_FORMAT) logger.info( f"Training {metadata['pair']}, {self.pair_it}/{self.total_pairs} pairs" f" from {tr_train_startts_str} to {tr_train_stopts_str}, {train_it}/{total_trains} " diff --git a/tests/data/test_history.py b/tests/data/test_history.py index 9709e7ad0..8081e984f 100644 --- a/tests/data/test_history.py +++ b/tests/data/test_history.py @@ -13,7 +13,7 @@ from pandas import DataFrame from pandas.testing import assert_frame_equal from freqtrade.configuration import TimeRange -from freqtrade.constants import AVAILABLE_DATAHANDLERS +from freqtrade.constants import AVAILABLE_DATAHANDLERS, DATETIME_PRINT_FORMAT from freqtrade.data.converter import ohlcv_to_dataframe from freqtrade.data.history.hdf5datahandler import HDF5DataHandler from freqtrade.data.history.history_utils import (_download_pair_history, _download_trades_history, @@ -386,7 +386,7 @@ def test_load_partial_missing(testdatadir, caplog) -> None: assert td != len(data['UNITTEST/BTC']) start_real = data['UNITTEST/BTC'].iloc[0, 0] assert log_has(f'UNITTEST/BTC, spot, 5m, ' - f'data starts at {start_real.strftime("%Y-%m-%d %H:%M:%S")}', + f'data starts at {start_real.strftime(DATETIME_PRINT_FORMAT)}', caplog) # Make sure we start fresh - test missing data at end caplog.clear() @@ -401,7 +401,7 @@ def test_load_partial_missing(testdatadir, caplog) -> None: # Shift endtime with +5 - as last candle is dropped (partial candle) end_real = arrow.get(data['UNITTEST/BTC'].iloc[-1, 0]).shift(minutes=5) assert log_has(f'UNITTEST/BTC, spot, 5m, ' - f'data ends at {end_real.strftime("%Y-%m-%d %H:%M:%S")}', + f'data ends at {end_real.strftime(DATETIME_PRINT_FORMAT)}', caplog) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index cdca3bc4d..e7f218c02 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -9,7 +9,7 @@ import arrow import pytest from sqlalchemy import create_engine, text -from freqtrade import constants +from freqtrade.constants import DATETIME_PRINT_FORMAT, DEFAULT_DB_PROD_URL from freqtrade.enums import TradingMode from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.persistence import LocalTrade, Order, Trade, init_db @@ -52,7 +52,7 @@ def test_init_invalid_db_url(): def test_init_prod_db(default_conf, mocker): default_conf.update({'dry_run': False}) - default_conf.update({'db_url': constants.DEFAULT_DB_PROD_URL}) + default_conf.update({'db_url': DEFAULT_DB_PROD_URL}) create_engine_mock = mocker.patch('freqtrade.persistence.models.create_engine', MagicMock()) @@ -1739,7 +1739,7 @@ def test_to_json(fee): 'base_currency': 'ADA', 'quote_currency': 'USDT', 'is_open': None, - 'open_date': trade.open_date.strftime("%Y-%m-%d %H:%M:%S"), + 'open_date': trade.open_date.strftime(DATETIME_PRINT_FORMAT), 'open_timestamp': int(trade.open_date.timestamp() * 1000), 'open_order_id': 'dry_run_buy_12345', 'close_date': None, @@ -1817,9 +1817,9 @@ def test_to_json(fee): 'pair': 'XRP/BTC', 'base_currency': 'XRP', 'quote_currency': 'BTC', - 'open_date': trade.open_date.strftime("%Y-%m-%d %H:%M:%S"), + 'open_date': trade.open_date.strftime(DATETIME_PRINT_FORMAT), 'open_timestamp': int(trade.open_date.timestamp() * 1000), - 'close_date': trade.close_date.strftime("%Y-%m-%d %H:%M:%S"), + 'close_date': trade.close_date.strftime(DATETIME_PRINT_FORMAT), 'close_timestamp': int(trade.close_date.timestamp() * 1000), 'open_rate': 0.123, 'close_rate': 0.125, From 939fb7acb338202dfa2bf945ee51d95e37e14e97 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 9 Sep 2022 20:31:30 +0200 Subject: [PATCH 23/33] Fix enum imports --- freqtrade/commands/db_commands.py | 2 +- freqtrade/exchange/okx.py | 3 +-- freqtrade/rpc/discord.py | 2 +- freqtrade/strategy/interface.py | 5 ++--- freqtrade/strategy/parameters.py | 2 +- tests/exchange/test_okx.py | 3 +-- tests/strategy/test_interface.py | 3 +-- 7 files changed, 8 insertions(+), 12 deletions(-) diff --git a/freqtrade/commands/db_commands.py b/freqtrade/commands/db_commands.py index 618b5cb6e..c424016b1 100644 --- a/freqtrade/commands/db_commands.py +++ b/freqtrade/commands/db_commands.py @@ -4,7 +4,7 @@ from typing import Any, Dict from sqlalchemy import func from freqtrade.configuration.config_setup import setup_utils_configuration -from freqtrade.enums.runmode import RunMode +from freqtrade.enums import RunMode logger = logging.getLogger(__name__) diff --git a/freqtrade/exchange/okx.py b/freqtrade/exchange/okx.py index 9340dd0e4..49f8ea107 100644 --- a/freqtrade/exchange/okx.py +++ b/freqtrade/exchange/okx.py @@ -4,8 +4,7 @@ from typing import Dict, List, Optional, Tuple import ccxt from freqtrade.constants import BuySell -from freqtrade.enums import MarginMode, TradingMode -from freqtrade.enums.candletype import CandleType +from freqtrade.enums import CandleType, MarginMode, TradingMode from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError from freqtrade.exchange import Exchange, date_minus_candles from freqtrade.exchange.common import retrier diff --git a/freqtrade/rpc/discord.py b/freqtrade/rpc/discord.py index 5991f7126..85acfae4e 100644 --- a/freqtrade/rpc/discord.py +++ b/freqtrade/rpc/discord.py @@ -1,7 +1,7 @@ import logging from typing import Any, Dict -from freqtrade.enums.rpcmessagetype import RPCMessageType +from freqtrade.enums import RPCMessageType from freqtrade.rpc import RPC from freqtrade.rpc.webhook import Webhook diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 70cc7fdb3..9401ebebe 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -12,9 +12,8 @@ from pandas import DataFrame from freqtrade.constants import ListPairsWithTimeframes from freqtrade.data.dataprovider import DataProvider -from freqtrade.enums import (CandleType, ExitCheckTuple, ExitType, SignalDirection, SignalTagType, - SignalType, TradingMode) -from freqtrade.enums.runmode import RunMode +from freqtrade.enums import (CandleType, ExitCheckTuple, ExitType, RunMode, SignalDirection, + SignalTagType, SignalType, TradingMode) from freqtrade.exceptions import OperationalException, StrategyError from freqtrade.exchange import timeframe_to_minutes, timeframe_to_next_date, timeframe_to_seconds from freqtrade.persistence import Order, PairLocks, Trade diff --git a/freqtrade/strategy/parameters.py b/freqtrade/strategy/parameters.py index c6037ae0b..796fb9514 100644 --- a/freqtrade/strategy/parameters.py +++ b/freqtrade/strategy/parameters.py @@ -7,7 +7,7 @@ from abc import ABC, abstractmethod from contextlib import suppress from typing import Any, Optional, Sequence, Union -from freqtrade.enums.hyperoptstate import HyperoptState +from freqtrade.enums import HyperoptState from freqtrade.optimize.hyperopt_tools import HyperoptStateContainer diff --git a/tests/exchange/test_okx.py b/tests/exchange/test_okx.py index b475b84ff..12322acae 100644 --- a/tests/exchange/test_okx.py +++ b/tests/exchange/test_okx.py @@ -4,8 +4,7 @@ from unittest.mock import MagicMock, PropertyMock import pytest -from freqtrade.enums import MarginMode, TradingMode -from freqtrade.enums.candletype import CandleType +from freqtrade.enums import CandleType, MarginMode, TradingMode from freqtrade.exchange.exchange import timeframe_to_minutes from tests.conftest import get_mock_coro, get_patched_exchange, log_has from tests.exchange.test_exchange import ccxt_exceptionhandlers diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 65ee05d71..070e78b1d 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -11,8 +11,7 @@ from pandas import DataFrame from freqtrade.configuration import TimeRange from freqtrade.data.dataprovider import DataProvider from freqtrade.data.history import load_data -from freqtrade.enums import ExitCheckTuple, ExitType, SignalDirection -from freqtrade.enums.hyperoptstate import HyperoptState +from freqtrade.enums import ExitCheckTuple, ExitType, HyperoptState, SignalDirection from freqtrade.exceptions import OperationalException, StrategyError from freqtrade.optimize.hyperopt_tools import HyperoptStateContainer from freqtrade.optimize.space import SKDecimal From 05581db4e387ae8c1b2ae7d097e3b20f349e4b16 Mon Sep 17 00:00:00 2001 From: robcaulk Date: Fri, 9 Sep 2022 20:37:21 +0200 Subject: [PATCH 24/33] ensure columns are only dropped in training --- freqtrade/freqai/data_kitchen.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/freqtrade/freqai/data_kitchen.py b/freqtrade/freqai/data_kitchen.py index 790b3c078..1af01cb6b 100644 --- a/freqtrade/freqai/data_kitchen.py +++ b/freqtrade/freqai/data_kitchen.py @@ -209,14 +209,13 @@ class FreqaiDataKitchen: filtered_df = unfiltered_df.filter(training_feature_list, axis=1) filtered_df = filtered_df.replace([np.inf, -np.inf], np.nan) - const_cols = list((filtered_df.nunique() == 1).loc[lambda x: x].index) - if const_cols: - filtered_df = filtered_df.filter(filtered_df.columns.difference(const_cols)) - logger.warning(f"Removed features {const_cols} with constant values.") - drop_index = pd.isnull(filtered_df).any(1) # get the rows that have NaNs, drop_index = drop_index.replace(True, 1).replace(False, 0) # pep8 requirement. if (training_filter): + const_cols = list((filtered_df.nunique() == 1).loc[lambda x: x].index) + if const_cols: + filtered_df = filtered_df.filter(filtered_df.columns.difference(const_cols)) + logger.warning(f"Removed features {const_cols} with constant values.") # we don't care about total row number (total no. datapoints) in training, we only care # about removing any row with NaNs # if labels has multiple columns (user wants to train multiple modelEs), we detect here From 170bec0438e10955052fe7782ede7a42a2310cc8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 10 Sep 2022 08:24:23 +0200 Subject: [PATCH 25/33] Fix failing XGBoost tests --- tests/freqai/test_freqai_interface.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index d95ead90c..5f8eeb086 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -174,7 +174,7 @@ def test_extract_data_and_train_model_LightGBMClassifier(mocker, freqai_conf): shutil.rmtree(Path(freqai.dk.full_path)) -def test_train_model_in_series_XGBoostRegressor(mocker, freqai_conf): +def test_extract_data_and_train_model_XGBoostRegressor(mocker, freqai_conf): freqai_conf.update({"timerange": "20180110-20180130"}) freqai_conf.update({"freqaimodel": "XGBoostRegressor"}) freqai_conf.update({"strategy": "freqai_test_strat"}) @@ -194,8 +194,8 @@ def test_train_model_in_series_XGBoostRegressor(mocker, freqai_conf): data_load_timerange = TimeRange.parse_timerange("20180110-20180130") new_timerange = TimeRange.parse_timerange("20180120-20180130") - freqai.train_model_in_series(new_timerange, "ADA/BTC", - strategy, freqai.dk, data_load_timerange) + freqai.extract_data_and_train_model( + new_timerange, "ADA/BTC", strategy, freqai.dk, data_load_timerange) assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_model.joblib").is_file() assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_metadata.json").is_file() @@ -205,7 +205,7 @@ def test_train_model_in_series_XGBoostRegressor(mocker, freqai_conf): shutil.rmtree(Path(freqai.dk.full_path)) -def test_train_model_in_series_XGBoostRegressorMultiModel(mocker, freqai_conf): +def test_extract_data_and_train_model_XGBoostRegressorMultiModel(mocker, freqai_conf): freqai_conf.update({"timerange": "20180110-20180130"}) freqai_conf.update({"freqaimodel": "XGBoostRegressorMultiTarget"}) freqai_conf.update({"strategy": "freqai_test_multimodel_strat"}) @@ -224,7 +224,8 @@ def test_train_model_in_series_XGBoostRegressorMultiModel(mocker, freqai_conf): data_load_timerange = TimeRange.parse_timerange("20180110-20180130") new_timerange = TimeRange.parse_timerange("20180120-20180130") - freqai.train_model_in_series(new_timerange, "ADA/BTC", strategy, freqai.dk, data_load_timerange) + freqai.extract_data_and_train_model( + new_timerange, "ADA/BTC", strategy, freqai.dk, data_load_timerange) assert len(freqai.dk.label_list) == 2 assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_model.joblib").is_file() From 79985fda015c0f060d0b0aca7d83962c43c75e8e Mon Sep 17 00:00:00 2001 From: Wagner Costa Santos Date: Sat, 10 Sep 2022 10:27:17 -0300 Subject: [PATCH 26/33] fix backtesting freqai startup candles bug --- freqtrade/freqai/data_kitchen.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/freqtrade/freqai/data_kitchen.py b/freqtrade/freqai/data_kitchen.py index 01b2f1f3b..88d841b9e 100644 --- a/freqtrade/freqai/data_kitchen.py +++ b/freqtrade/freqai/data_kitchen.py @@ -468,8 +468,14 @@ class FreqaiDataKitchen: Function which takes the backtesting time range and remove training data from dataframe """ + startup_candle_count = self.config.get('startup_candle_count', 0) + tf = self.config['timeframe'] tr = self.config["timerange"] + backtesting_timerange = TimeRange.parse_timerange(tr) + if startup_candle_count > 0 and backtesting_timerange: + backtesting_timerange.subtract_start(timeframe_to_seconds(tf) * startup_candle_count) + start = datetime.fromtimestamp(backtesting_timerange.startts, tz=timezone.utc) df = self.return_dataframe df = df.loc[df["date"] >= start, :] From daf352e6a5dd660e247a38a1a209ff34561ba7a3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 10 Sep 2022 16:01:06 +0200 Subject: [PATCH 27/33] Test online candle fetching in futures, too disable gateio checking on spot markets --- tests/exchange/test_ccxt_compat.py | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/tests/exchange/test_ccxt_compat.py b/tests/exchange/test_ccxt_compat.py index 49b7684f8..f57b0b366 100644 --- a/tests/exchange/test_ccxt_compat.py +++ b/tests/exchange/test_ccxt_compat.py @@ -267,13 +267,8 @@ class TestCCXTExchange(): now = datetime.now(timezone.utc) - timedelta(minutes=(timeframe_to_minutes(timeframe) * 2)) assert exchange.klines(pair_tf).iloc[-1]['date'] >= timeframe_to_prev_date(timeframe, now) - def test_ccxt__async_get_candle_history(self, exchange): - exchange, exchangename = exchange - # For some weired reason, this test returns random lengths for bittrex. - if not exchange._ft_has['ohlcv_has_history'] or exchangename == 'bittrex': - return - pair = EXCHANGES[exchangename]['pair'] - timeframe = EXCHANGES[exchangename]['timeframe'] + def ccxt__async_get_candle_history(self, exchange, exchangename, pair, timeframe): + candle_type = CandleType.SPOT timeframe_ms = timeframe_to_msecs(timeframe) now = timeframe_to_prev_date( @@ -299,6 +294,24 @@ class TestCCXTExchange(): assert len(candles) >= min(candle_count, candle_count1) assert candles[0][0] == since_ms or (since_ms + timeframe_ms) + def test_ccxt__async_get_candle_history(self, exchange): + exchange, exchangename = exchange + # For some weired reason, this test returns random lengths for bittrex. + if not exchange._ft_has['ohlcv_has_history'] or exchangename in ('bittrex', 'gateio'): + return + pair = EXCHANGES[exchangename]['pair'] + timeframe = EXCHANGES[exchangename]['timeframe'] + self.ccxt__async_get_candle_history(exchange, exchangename, pair, timeframe) + + def test_ccxt__async_get_candle_history_futures(self, exchange_futures): + exchange, exchangename = exchange_futures + if not exchange: + # exchange_futures only returns values for supported exchanges + return + pair = EXCHANGES[exchangename].get('futures_pair', EXCHANGES[exchangename]['pair']) + timeframe = EXCHANGES[exchangename]['timeframe'] + self.ccxt__async_get_candle_history(exchange, exchangename, pair, timeframe) + def test_ccxt_fetch_funding_rate_history(self, exchange_futures): exchange, exchangename = exchange_futures if not exchange: From 10b6aebc5f5ada8b1e6aef733f7c8ccf4e29e8ba Mon Sep 17 00:00:00 2001 From: robcaulk Date: Sat, 10 Sep 2022 16:54:13 +0200 Subject: [PATCH 28/33] enable continual learning and evaluation sets on multioutput models. --- .../BaseClassifierModel.py | 0 .../BaseRegressionModel.py | 0 .../BaseTensorFlowModel.py | 0 .../base_models/FreqaiMultiOutputRegressor.py | 75 +++++++++++++++++++ .../prediction_models/CatboostClassifier.py | 2 +- .../prediction_models/CatboostRegressor.py | 2 +- .../CatboostRegressorMultiTarget.py | 41 +++++++--- .../prediction_models/LightGBMClassifier.py | 2 +- .../prediction_models/LightGBMRegressor.py | 2 +- .../LightGBMRegressorMultiTarget.py | 41 +++++++--- .../prediction_models/XGBoostRegressor.py | 6 +- .../XGBoostRegressorMultiTarget.py | 37 ++++++--- 12 files changed, 170 insertions(+), 38 deletions(-) rename freqtrade/freqai/{prediction_models => base_models}/BaseClassifierModel.py (100%) rename freqtrade/freqai/{prediction_models => base_models}/BaseRegressionModel.py (100%) rename freqtrade/freqai/{prediction_models => base_models}/BaseTensorFlowModel.py (100%) create mode 100644 freqtrade/freqai/base_models/FreqaiMultiOutputRegressor.py diff --git a/freqtrade/freqai/prediction_models/BaseClassifierModel.py b/freqtrade/freqai/base_models/BaseClassifierModel.py similarity index 100% rename from freqtrade/freqai/prediction_models/BaseClassifierModel.py rename to freqtrade/freqai/base_models/BaseClassifierModel.py diff --git a/freqtrade/freqai/prediction_models/BaseRegressionModel.py b/freqtrade/freqai/base_models/BaseRegressionModel.py similarity index 100% rename from freqtrade/freqai/prediction_models/BaseRegressionModel.py rename to freqtrade/freqai/base_models/BaseRegressionModel.py diff --git a/freqtrade/freqai/prediction_models/BaseTensorFlowModel.py b/freqtrade/freqai/base_models/BaseTensorFlowModel.py similarity index 100% rename from freqtrade/freqai/prediction_models/BaseTensorFlowModel.py rename to freqtrade/freqai/base_models/BaseTensorFlowModel.py diff --git a/freqtrade/freqai/base_models/FreqaiMultiOutputRegressor.py b/freqtrade/freqai/base_models/FreqaiMultiOutputRegressor.py new file mode 100644 index 000000000..aa5dbe629 --- /dev/null +++ b/freqtrade/freqai/base_models/FreqaiMultiOutputRegressor.py @@ -0,0 +1,75 @@ + +from joblib import Parallel +from sklearn.multioutput import MultiOutputRegressor, _fit_estimator +from sklearn.utils.fixes import delayed +from sklearn.utils.validation import has_fit_parameter + + +class FreqaiMultiOutputRegressor(MultiOutputRegressor): + + def fit(self, X, y, sample_weight=None, fit_params=None): + """Fit the model to data, separately for each output variable. + Parameters + ---------- + X : {array-like, sparse matrix} of shape (n_samples, n_features) + The input data. + y : {array-like, sparse matrix} of shape (n_samples, n_outputs) + Multi-output targets. An indicator matrix turns on multilabel + estimation. + sample_weight : array-like of shape (n_samples,), default=None + Sample weights. If `None`, then samples are equally weighted. + Only supported if the underlying regressor supports sample + weights. + fit_params : A list of dicts for the fit_params + Parameters passed to the ``estimator.fit`` method of each step. + Each dict may contain same or different values (e.g. different + eval_sets or init_models) + .. versionadded:: 0.23 + Returns + ------- + self : object + Returns a fitted instance. + """ + + if not hasattr(self.estimator, "fit"): + raise ValueError("The base estimator should implement a fit method") + + y = self._validate_data(X="no_validation", y=y, multi_output=True) + + # if is_classifier(self): + # check_classification_targets(y) + + if y.ndim == 1: + raise ValueError( + "y must have at least two dimensions for " + "multi-output regression but has only one." + ) + + if sample_weight is not None and not has_fit_parameter( + self.estimator, "sample_weight" + ): + raise ValueError("Underlying estimator does not support sample weights.") + + # fit_params_validated = _check_fit_params(X, fit_params) + + if not fit_params: + fit_params = [None] * y.shape[1] + + # if not init_models: + # init_models = [None] * y.shape[1] + + self.estimators_ = Parallel(n_jobs=self.n_jobs)( + delayed(_fit_estimator)( + self.estimator, X, y[:, i], sample_weight, **fit_params[i] + # init_model=init_models[i], eval_set=eval_sets[i], + # **fit_params_validated + ) + for i in range(y.shape[1]) + ) + + if hasattr(self.estimators_[0], "n_features_in_"): + self.n_features_in_ = self.estimators_[0].n_features_in_ + if hasattr(self.estimators_[0], "feature_names_in_"): + self.feature_names_in_ = self.estimators_[0].feature_names_in_ + + return diff --git a/freqtrade/freqai/prediction_models/CatboostClassifier.py b/freqtrade/freqai/prediction_models/CatboostClassifier.py index cd7afd392..60536e6de 100644 --- a/freqtrade/freqai/prediction_models/CatboostClassifier.py +++ b/freqtrade/freqai/prediction_models/CatboostClassifier.py @@ -3,8 +3,8 @@ from typing import Any, Dict from catboost import CatBoostClassifier, Pool +from freqtrade.freqai.base_models.BaseClassifierModel import BaseClassifierModel from freqtrade.freqai.data_kitchen import FreqaiDataKitchen -from freqtrade.freqai.prediction_models.BaseClassifierModel import BaseClassifierModel logger = logging.getLogger(__name__) diff --git a/freqtrade/freqai/prediction_models/CatboostRegressor.py b/freqtrade/freqai/prediction_models/CatboostRegressor.py index 1ce31b628..73cf6c88a 100644 --- a/freqtrade/freqai/prediction_models/CatboostRegressor.py +++ b/freqtrade/freqai/prediction_models/CatboostRegressor.py @@ -3,8 +3,8 @@ from typing import Any, Dict from catboost import CatBoostRegressor, Pool +from freqtrade.freqai.base_models.BaseRegressionModel import BaseRegressionModel from freqtrade.freqai.data_kitchen import FreqaiDataKitchen -from freqtrade.freqai.prediction_models.BaseRegressionModel import BaseRegressionModel logger = logging.getLogger(__name__) diff --git a/freqtrade/freqai/prediction_models/CatboostRegressorMultiTarget.py b/freqtrade/freqai/prediction_models/CatboostRegressorMultiTarget.py index bc52bfdd9..a376b2c33 100644 --- a/freqtrade/freqai/prediction_models/CatboostRegressorMultiTarget.py +++ b/freqtrade/freqai/prediction_models/CatboostRegressorMultiTarget.py @@ -1,11 +1,11 @@ import logging from typing import Any, Dict -from catboost import CatBoostRegressor # , Pool -from sklearn.multioutput import MultiOutputRegressor +from catboost import CatBoostRegressor, Pool +from freqtrade.freqai.base_models.BaseRegressionModel import BaseRegressionModel +from freqtrade.freqai.base_models.FreqaiMultiOutputRegressor import FreqaiMultiOutputRegressor from freqtrade.freqai.data_kitchen import FreqaiDataKitchen -from freqtrade.freqai.prediction_models.BaseRegressionModel import BaseRegressionModel logger = logging.getLogger(__name__) @@ -32,17 +32,34 @@ class CatboostRegressorMultiTarget(BaseRegressionModel): X = data_dictionary["train_features"] y = data_dictionary["train_labels"] - eval_set = (data_dictionary["test_features"], data_dictionary["test_labels"]) + sample_weight = data_dictionary["train_weights"] - if self.continual_learning: - logger.warning('Continual learning not supported for MultiTarget models') - - model = MultiOutputRegressor(estimator=cbr) - model.fit(X=X, y=y, sample_weight=sample_weight) # , eval_set=eval_set) + eval_sets = [None] * y.shape[1] if self.freqai_info.get('data_split_parameters', {}).get('test_size', 0.1) != 0: - train_score = model.score(X, y) - test_score = model.score(*eval_set) - logger.info(f"Train score {train_score}, Test score {test_score}") + eval_sets = [None] * data_dictionary['test_labels'].shape[1] + + for i in range(data_dictionary['test_labels'].shape[1]): + eval_sets[i] = Pool( + data=data_dictionary["test_features"], + label=data_dictionary["test_labels"].iloc[:, i], + weight=data_dictionary["test_weights"], + ) + + init_model = self.get_init_model(dk.pair) + + if init_model: + init_models = init_model.estimators_ + else: + init_models = [None] * y.shape[1] + + fit_params = [] + for i in range(len(eval_sets)): + fit_params.append( + {'eval_set': eval_sets[i], 'init_model': init_models[i]}) + + model = FreqaiMultiOutputRegressor(estimator=cbr) + model.fit(X=X, y=y, sample_weight=sample_weight, fit_params=fit_params) + return model diff --git a/freqtrade/freqai/prediction_models/LightGBMClassifier.py b/freqtrade/freqai/prediction_models/LightGBMClassifier.py index 69867eae3..3eec516ba 100644 --- a/freqtrade/freqai/prediction_models/LightGBMClassifier.py +++ b/freqtrade/freqai/prediction_models/LightGBMClassifier.py @@ -3,8 +3,8 @@ from typing import Any, Dict from lightgbm import LGBMClassifier +from freqtrade.freqai.base_models.BaseClassifierModel import BaseClassifierModel from freqtrade.freqai.data_kitchen import FreqaiDataKitchen -from freqtrade.freqai.prediction_models.BaseClassifierModel import BaseClassifierModel logger = logging.getLogger(__name__) diff --git a/freqtrade/freqai/prediction_models/LightGBMRegressor.py b/freqtrade/freqai/prediction_models/LightGBMRegressor.py index 99e9ff887..85c9b691c 100644 --- a/freqtrade/freqai/prediction_models/LightGBMRegressor.py +++ b/freqtrade/freqai/prediction_models/LightGBMRegressor.py @@ -3,8 +3,8 @@ from typing import Any, Dict from lightgbm import LGBMRegressor +from freqtrade.freqai.base_models.BaseRegressionModel import BaseRegressionModel from freqtrade.freqai.data_kitchen import FreqaiDataKitchen -from freqtrade.freqai.prediction_models.BaseRegressionModel import BaseRegressionModel logger = logging.getLogger(__name__) diff --git a/freqtrade/freqai/prediction_models/LightGBMRegressorMultiTarget.py b/freqtrade/freqai/prediction_models/LightGBMRegressorMultiTarget.py index c34680dbe..7a9b5c36a 100644 --- a/freqtrade/freqai/prediction_models/LightGBMRegressorMultiTarget.py +++ b/freqtrade/freqai/prediction_models/LightGBMRegressorMultiTarget.py @@ -2,10 +2,10 @@ import logging from typing import Any, Dict from lightgbm import LGBMRegressor -from sklearn.multioutput import MultiOutputRegressor +from freqtrade.freqai.base_models.BaseRegressionModel import BaseRegressionModel +from freqtrade.freqai.base_models.FreqaiMultiOutputRegressor import FreqaiMultiOutputRegressor from freqtrade.freqai.data_kitchen import FreqaiDataKitchen -from freqtrade.freqai.prediction_models.BaseRegressionModel import BaseRegressionModel logger = logging.getLogger(__name__) @@ -29,15 +29,36 @@ class LightGBMRegressorMultiTarget(BaseRegressionModel): X = data_dictionary["train_features"] y = data_dictionary["train_labels"] - eval_set = (data_dictionary["test_features"], data_dictionary["test_labels"]) sample_weight = data_dictionary["train_weights"] - if self.continual_learning: - logger.warning('Continual learning not supported for MultiTarget models') + eval_weights = None + eval_sets = [None] * y.shape[1] - model = MultiOutputRegressor(estimator=lgb) - model.fit(X=X, y=y, sample_weight=sample_weight) # , eval_set=eval_set) - train_score = model.score(X, y) - test_score = model.score(*eval_set) - logger.info(f"Train score {train_score}, Test score {test_score}") + if self.freqai_info.get('data_split_parameters', {}).get('test_size', 0.1) != 0: + eval_weights = [data_dictionary["test_weights"]] + eval_sets = [(None, None)] * data_dictionary['test_labels'].shape[1] # type: ignore + for i in range(data_dictionary['test_labels'].shape[1]): + eval_sets[i] = ( # type: ignore + data_dictionary["test_features"], + data_dictionary["test_labels"].iloc[:, i] + ) + + init_model = self.get_init_model(dk.pair) + if init_model: + init_models = init_model.estimators_ + else: + init_models = [None] * y.shape[1] + + fit_params = [] + for i in range(len(eval_sets)): + fit_params.append( + {'eval_set': eval_sets[i], 'eval_sample_weight': eval_weights, + 'init_model': init_models[i]}) + + model = FreqaiMultiOutputRegressor(estimator=lgb) + model.fit(X=X, y=y, sample_weight=sample_weight, fit_params=fit_params) + + # model = FreqaiMultiOutputRegressor(estimator=lgb) + # model.fit(X=X, y=y, sample_weight=sample_weight, init_models=init_models, + # eval_sets=eval_sets, eval_sample_weight=eval_weights) return model diff --git a/freqtrade/freqai/prediction_models/XGBoostRegressor.py b/freqtrade/freqai/prediction_models/XGBoostRegressor.py index acc4386f5..c9be9ce74 100644 --- a/freqtrade/freqai/prediction_models/XGBoostRegressor.py +++ b/freqtrade/freqai/prediction_models/XGBoostRegressor.py @@ -3,8 +3,8 @@ from typing import Any, Dict from xgboost import XGBRegressor +from freqtrade.freqai.base_models.BaseRegressionModel import BaseRegressionModel from freqtrade.freqai.data_kitchen import FreqaiDataKitchen -from freqtrade.freqai.prediction_models.BaseRegressionModel import BaseRegressionModel logger = logging.getLogger(__name__) @@ -31,6 +31,7 @@ class XGBoostRegressor(BaseRegressionModel): eval_set = None else: eval_set = [(data_dictionary["test_features"], data_dictionary["test_labels"])] + eval_weights = [data_dictionary['test_weights']] sample_weight = data_dictionary["train_weights"] @@ -38,6 +39,7 @@ class XGBoostRegressor(BaseRegressionModel): model = XGBRegressor(**self.model_training_parameters) - model.fit(X=X, y=y, sample_weight=sample_weight, eval_set=eval_set, xgb_model=xgb_model) + model.fit(X=X, y=y, sample_weight=sample_weight, eval_set=eval_set, + sample_weight_eval_set=eval_weights, xgb_model=xgb_model) return model diff --git a/freqtrade/freqai/prediction_models/XGBoostRegressorMultiTarget.py b/freqtrade/freqai/prediction_models/XGBoostRegressorMultiTarget.py index 5283501d1..38c478c0b 100644 --- a/freqtrade/freqai/prediction_models/XGBoostRegressorMultiTarget.py +++ b/freqtrade/freqai/prediction_models/XGBoostRegressorMultiTarget.py @@ -1,11 +1,11 @@ import logging from typing import Any, Dict -from sklearn.multioutput import MultiOutputRegressor from xgboost import XGBRegressor +from freqtrade.freqai.base_models.BaseRegressionModel import BaseRegressionModel +from freqtrade.freqai.base_models.FreqaiMultiOutputRegressor import FreqaiMultiOutputRegressor from freqtrade.freqai.data_kitchen import FreqaiDataKitchen -from freqtrade.freqai.prediction_models.BaseRegressionModel import BaseRegressionModel logger = logging.getLogger(__name__) @@ -29,15 +29,32 @@ class XGBoostRegressorMultiTarget(BaseRegressionModel): X = data_dictionary["train_features"] y = data_dictionary["train_labels"] - eval_set = (data_dictionary["test_features"], data_dictionary["test_labels"]) sample_weight = data_dictionary["train_weights"] - if self.continual_learning: - logger.warning('Continual learning not supported for MultiTarget models') + eval_weights = None + eval_sets = [None] * y.shape[1] + + if self.freqai_info.get('data_split_parameters', {}).get('test_size', 0.1) != 0: + eval_weights = [data_dictionary["test_weights"]] + for i in range(data_dictionary['test_labels'].shape[1]): + eval_sets[i] = [( # type: ignore + data_dictionary["test_features"], + data_dictionary["test_labels"].iloc[:, i] + )] + + init_model = self.get_init_model(dk.pair) + if init_model: + init_models = init_model.estimators_ + else: + init_models = [None] * y.shape[1] + + fit_params = [] + for i in range(len(eval_sets)): + fit_params.append( + {'eval_set': eval_sets[i], 'sample_weight_eval_set': eval_weights, + 'xgb_model': init_models[i]}) + + model = FreqaiMultiOutputRegressor(estimator=xgb) + model.fit(X=X, y=y, sample_weight=sample_weight, fit_params=fit_params) - model = MultiOutputRegressor(estimator=xgb) - model.fit(X=X, y=y, sample_weight=sample_weight) # , eval_set=eval_set) - train_score = model.score(X, y) - test_score = model.score(*eval_set) - logger.info(f"Train score {train_score}, Test score {test_score}") return model From 311ae8bf1f14e4cc80d6e5ec036c942d9af716bf Mon Sep 17 00:00:00 2001 From: Wagner Costa Santos Date: Sat, 10 Sep 2022 14:45:42 -0300 Subject: [PATCH 29/33] freqai backtesting - add startup_candle_count at function description --- freqtrade/freqai/data_kitchen.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/freqai/data_kitchen.py b/freqtrade/freqai/data_kitchen.py index 88d841b9e..9168db2aa 100644 --- a/freqtrade/freqai/data_kitchen.py +++ b/freqtrade/freqai/data_kitchen.py @@ -466,7 +466,8 @@ class FreqaiDataKitchen: ) -> DataFrame: """ Function which takes the backtesting time range and - remove training data from dataframe + remove training data from dataframe, keeping only the + startup_candle_count candles """ startup_candle_count = self.config.get('startup_candle_count', 0) tf = self.config['timeframe'] From f97f1dc5c38bd7e3c0b731404040a2a8e56a20e5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 10 Sep 2022 19:57:21 +0200 Subject: [PATCH 30/33] Test CatboostRegressorMultiTarget, simplify test setup via parametrization --- tests/freqai/test_freqai_interface.py | 44 ++++++--------------------- 1 file changed, 10 insertions(+), 34 deletions(-) diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index 5f8eeb086..115ad0ea8 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -46,10 +46,18 @@ def test_extract_data_and_train_model_LightGBM(mocker, freqai_conf): shutil.rmtree(Path(freqai.dk.full_path)) -def test_extract_data_and_train_model_LightGBMMultiModel(mocker, freqai_conf): +@pytest.mark.parametrize('model', [ + 'LightGBMRegressorMultiTarget', + 'XGBoostRegressorMultiTarget', + 'CatboostRegressorMultiTarget', + ]) +def test_extract_data_and_train_model_MultiTargets(mocker, freqai_conf, model): + if is_arm() and model == 'CatboostRegressorMultiTarget': + pytest.skip("CatBoost is not supported on ARM") + freqai_conf.update({"timerange": "20180110-20180130"}) freqai_conf.update({"strategy": "freqai_test_multimodel_strat"}) - freqai_conf.update({"freqaimodel": "LightGBMRegressorMultiTarget"}) + freqai_conf.update({"freqaimodel": model}) strategy = get_patched_freqai_strategy(mocker, freqai_conf) exchange = get_patched_exchange(mocker, freqai_conf) strategy.dp = DataProvider(freqai_conf, exchange) @@ -205,38 +213,6 @@ def test_extract_data_and_train_model_XGBoostRegressor(mocker, freqai_conf): shutil.rmtree(Path(freqai.dk.full_path)) -def test_extract_data_and_train_model_XGBoostRegressorMultiModel(mocker, freqai_conf): - freqai_conf.update({"timerange": "20180110-20180130"}) - freqai_conf.update({"freqaimodel": "XGBoostRegressorMultiTarget"}) - freqai_conf.update({"strategy": "freqai_test_multimodel_strat"}) - strategy = get_patched_freqai_strategy(mocker, freqai_conf) - exchange = get_patched_exchange(mocker, freqai_conf) - strategy.dp = DataProvider(freqai_conf, exchange) - strategy.freqai_info = freqai_conf.get("freqai", {}) - freqai = strategy.freqai - freqai.live = True - freqai.dk = FreqaiDataKitchen(freqai_conf) - timerange = TimeRange.parse_timerange("20180110-20180130") - freqai.dd.load_all_pair_histories(timerange, freqai.dk) - - freqai.dd.pair_dict = MagicMock() - - data_load_timerange = TimeRange.parse_timerange("20180110-20180130") - new_timerange = TimeRange.parse_timerange("20180120-20180130") - - freqai.extract_data_and_train_model( - new_timerange, "ADA/BTC", strategy, freqai.dk, data_load_timerange) - - assert len(freqai.dk.label_list) == 2 - assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_model.joblib").is_file() - assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_metadata.json").is_file() - assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_trained_df.pkl").is_file() - assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_svm_model.joblib").is_file() - assert len(freqai.dk.data['training_features_list']) == 26 - - shutil.rmtree(Path(freqai.dk.full_path)) - - def test_start_backtesting(mocker, freqai_conf): freqai_conf.update({"timerange": "20180120-20180130"}) freqai_conf.get("freqai", {}).update({"save_backtest_models": True}) From 88892ba663ea8805e06ecd2d101c8982f595ad5f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 10 Sep 2022 20:06:52 +0200 Subject: [PATCH 31/33] Parametrize regressor tests --- tests/freqai/test_freqai_interface.py | 76 ++++----------------------- 1 file changed, 11 insertions(+), 65 deletions(-) diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index 115ad0ea8..bd921c16b 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -17,8 +17,18 @@ def is_arm() -> bool: return "arm" in machine or "aarch64" in machine -def test_extract_data_and_train_model_LightGBM(mocker, freqai_conf): +@pytest.mark.parametrize('model', [ + 'LightGBMRegressor', + 'XGBoostRegressor', + 'CatboostRegressor', + ]) +def test_extract_data_and_train_model_Regressors(mocker, freqai_conf, model): + if is_arm() and model == 'CatboostRegressor': + pytest.skip("CatBoost is not supported on ARM") + + freqai_conf.update({"freqaimodel": model}) freqai_conf.update({"timerange": "20180110-20180130"}) + freqai_conf.update({"strategy": "freqai_test_strat"}) strategy = get_patched_freqai_strategy(mocker, freqai_conf) exchange = get_patched_exchange(mocker, freqai_conf) @@ -86,39 +96,6 @@ def test_extract_data_and_train_model_MultiTargets(mocker, freqai_conf, model): shutil.rmtree(Path(freqai.dk.full_path)) -@pytest.mark.skipif(is_arm(), reason="no ARM for Catboost ...") -def test_extract_data_and_train_model_Catboost(mocker, freqai_conf): - freqai_conf.update({"timerange": "20180110-20180130"}) - freqai_conf.update({"freqaimodel": "CatboostRegressor"}) - # freqai_conf.get('freqai', {}).update( - # {'model_training_parameters': {"n_estimators": 100, "verbose": 0}}) - strategy = get_patched_freqai_strategy(mocker, freqai_conf) - exchange = get_patched_exchange(mocker, freqai_conf) - strategy.dp = DataProvider(freqai_conf, exchange) - - strategy.freqai_info = freqai_conf.get("freqai", {}) - freqai = strategy.freqai - freqai.live = True - freqai.dk = FreqaiDataKitchen(freqai_conf) - timerange = TimeRange.parse_timerange("20180110-20180130") - freqai.dd.load_all_pair_histories(timerange, freqai.dk) - - freqai.dd.pair_dict = MagicMock() - - data_load_timerange = TimeRange.parse_timerange("20180110-20180130") - new_timerange = TimeRange.parse_timerange("20180120-20180130") - - freqai.extract_data_and_train_model(new_timerange, "ADA/BTC", - strategy, freqai.dk, data_load_timerange) - - assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_model.joblib").exists() - assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_metadata.json").exists() - assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_trained_df.pkl").exists() - assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_svm_model.joblib").exists() - - shutil.rmtree(Path(freqai.dk.full_path)) - - @pytest.mark.skipif(is_arm(), reason="no ARM for Catboost ...") def test_extract_data_and_train_model_CatboostClassifier(mocker, freqai_conf): freqai_conf.update({"timerange": "20180110-20180130"}) @@ -182,37 +159,6 @@ def test_extract_data_and_train_model_LightGBMClassifier(mocker, freqai_conf): shutil.rmtree(Path(freqai.dk.full_path)) -def test_extract_data_and_train_model_XGBoostRegressor(mocker, freqai_conf): - freqai_conf.update({"timerange": "20180110-20180130"}) - freqai_conf.update({"freqaimodel": "XGBoostRegressor"}) - freqai_conf.update({"strategy": "freqai_test_strat"}) - - strategy = get_patched_freqai_strategy(mocker, freqai_conf) - exchange = get_patched_exchange(mocker, freqai_conf) - strategy.dp = DataProvider(freqai_conf, exchange) - strategy.freqai_info = freqai_conf.get("freqai", {}) - freqai = strategy.freqai - freqai.live = True - freqai.dk = FreqaiDataKitchen(freqai_conf) - timerange = TimeRange.parse_timerange("20180110-20180130") - freqai.dd.load_all_pair_histories(timerange, freqai.dk) - - freqai.dd.pair_dict = MagicMock() - - data_load_timerange = TimeRange.parse_timerange("20180110-20180130") - new_timerange = TimeRange.parse_timerange("20180120-20180130") - - freqai.extract_data_and_train_model( - new_timerange, "ADA/BTC", strategy, freqai.dk, data_load_timerange) - - assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_model.joblib").is_file() - assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_metadata.json").is_file() - assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_trained_df.pkl").is_file() - assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_svm_model.joblib").is_file() - - shutil.rmtree(Path(freqai.dk.full_path)) - - def test_start_backtesting(mocker, freqai_conf): freqai_conf.update({"timerange": "20180120-20180130"}) freqai_conf.get("freqai", {}).update({"save_backtest_models": True}) From b3fc1cfde94b9b8e67e79d1f35f4ce3f36a4e5e0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 10 Sep 2022 20:17:57 +0200 Subject: [PATCH 32/33] Parametrize classifier tests --- tests/freqai/test_freqai_interface.py | 43 ++++++--------------------- 1 file changed, 9 insertions(+), 34 deletions(-) diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index bd921c16b..2a7cfeb73 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -96,42 +96,17 @@ def test_extract_data_and_train_model_MultiTargets(mocker, freqai_conf, model): shutil.rmtree(Path(freqai.dk.full_path)) -@pytest.mark.skipif(is_arm(), reason="no ARM for Catboost ...") -def test_extract_data_and_train_model_CatboostClassifier(mocker, freqai_conf): - freqai_conf.update({"timerange": "20180110-20180130"}) - freqai_conf.update({"freqaimodel": "CatboostClassifier"}) +@pytest.mark.parametrize('model', [ + 'LightGBMClassifier', + 'CatboostClassifier', + ]) +def test_extract_data_and_train_model_Classifiers(mocker, freqai_conf, model): + if is_arm() and model == 'CatboostClassifier': + pytest.skip("CatBoost is not supported on ARM") + + freqai_conf.update({"freqaimodel": model}) freqai_conf.update({"strategy": "freqai_test_classifier"}) - strategy = get_patched_freqai_strategy(mocker, freqai_conf) - exchange = get_patched_exchange(mocker, freqai_conf) - strategy.dp = DataProvider(freqai_conf, exchange) - - strategy.freqai_info = freqai_conf.get("freqai", {}) - freqai = strategy.freqai - freqai.live = True - freqai.dk = FreqaiDataKitchen(freqai_conf) - timerange = TimeRange.parse_timerange("20180110-20180130") - freqai.dd.load_all_pair_histories(timerange, freqai.dk) - - freqai.dd.pair_dict = MagicMock() - - data_load_timerange = TimeRange.parse_timerange("20180110-20180130") - new_timerange = TimeRange.parse_timerange("20180120-20180130") - - freqai.extract_data_and_train_model(new_timerange, "ADA/BTC", - strategy, freqai.dk, data_load_timerange) - - assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_model.joblib").exists() - assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_metadata.json").exists() - assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_trained_df.pkl").exists() - assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_svm_model.joblib").exists() - - shutil.rmtree(Path(freqai.dk.full_path)) - - -def test_extract_data_and_train_model_LightGBMClassifier(mocker, freqai_conf): freqai_conf.update({"timerange": "20180110-20180130"}) - freqai_conf.update({"freqaimodel": "LightGBMClassifier"}) - freqai_conf.update({"strategy": "freqai_test_classifier"}) strategy = get_patched_freqai_strategy(mocker, freqai_conf) exchange = get_patched_exchange(mocker, freqai_conf) strategy.dp = DataProvider(freqai_conf, exchange) From 5a0cfee27e8de330d00168b6de86a2ac84e0ed7f Mon Sep 17 00:00:00 2001 From: robcaulk Date: Sat, 10 Sep 2022 22:16:49 +0200 Subject: [PATCH 33/33] allow user to multithread jobs (advanced users only) --- .../freqai/base_models/FreqaiMultiOutputRegressor.py | 10 ---------- .../prediction_models/CatboostRegressorMultiTarget.py | 3 +++ .../prediction_models/LightGBMRegressorMultiTarget.py | 6 +++--- .../prediction_models/XGBoostRegressorMultiTarget.py | 3 +++ 4 files changed, 9 insertions(+), 13 deletions(-) diff --git a/freqtrade/freqai/base_models/FreqaiMultiOutputRegressor.py b/freqtrade/freqai/base_models/FreqaiMultiOutputRegressor.py index aa5dbe629..a9db81e31 100644 --- a/freqtrade/freqai/base_models/FreqaiMultiOutputRegressor.py +++ b/freqtrade/freqai/base_models/FreqaiMultiOutputRegressor.py @@ -36,9 +36,6 @@ class FreqaiMultiOutputRegressor(MultiOutputRegressor): y = self._validate_data(X="no_validation", y=y, multi_output=True) - # if is_classifier(self): - # check_classification_targets(y) - if y.ndim == 1: raise ValueError( "y must have at least two dimensions for " @@ -50,19 +47,12 @@ class FreqaiMultiOutputRegressor(MultiOutputRegressor): ): raise ValueError("Underlying estimator does not support sample weights.") - # fit_params_validated = _check_fit_params(X, fit_params) - if not fit_params: fit_params = [None] * y.shape[1] - # if not init_models: - # init_models = [None] * y.shape[1] - self.estimators_ = Parallel(n_jobs=self.n_jobs)( delayed(_fit_estimator)( self.estimator, X, y[:, i], sample_weight, **fit_params[i] - # init_model=init_models[i], eval_set=eval_sets[i], - # **fit_params_validated ) for i in range(y.shape[1]) ) diff --git a/freqtrade/freqai/prediction_models/CatboostRegressorMultiTarget.py b/freqtrade/freqai/prediction_models/CatboostRegressorMultiTarget.py index a376b2c33..7fa4e293e 100644 --- a/freqtrade/freqai/prediction_models/CatboostRegressorMultiTarget.py +++ b/freqtrade/freqai/prediction_models/CatboostRegressorMultiTarget.py @@ -60,6 +60,9 @@ class CatboostRegressorMultiTarget(BaseRegressionModel): {'eval_set': eval_sets[i], 'init_model': init_models[i]}) model = FreqaiMultiOutputRegressor(estimator=cbr) + thread_training = self.freqai_info.get('multitarget_parallel_training', False) + if thread_training: + model.n_jobs = y.shape[1] model.fit(X=X, y=y, sample_weight=sample_weight, fit_params=fit_params) return model diff --git a/freqtrade/freqai/prediction_models/LightGBMRegressorMultiTarget.py b/freqtrade/freqai/prediction_models/LightGBMRegressorMultiTarget.py index 7a9b5c36a..37c6bb186 100644 --- a/freqtrade/freqai/prediction_models/LightGBMRegressorMultiTarget.py +++ b/freqtrade/freqai/prediction_models/LightGBMRegressorMultiTarget.py @@ -56,9 +56,9 @@ class LightGBMRegressorMultiTarget(BaseRegressionModel): 'init_model': init_models[i]}) model = FreqaiMultiOutputRegressor(estimator=lgb) + thread_training = self.freqai_info.get('multitarget_parallel_training', False) + if thread_training: + model.n_jobs = y.shape[1] model.fit(X=X, y=y, sample_weight=sample_weight, fit_params=fit_params) - # model = FreqaiMultiOutputRegressor(estimator=lgb) - # model.fit(X=X, y=y, sample_weight=sample_weight, init_models=init_models, - # eval_sets=eval_sets, eval_sample_weight=eval_weights) return model diff --git a/freqtrade/freqai/prediction_models/XGBoostRegressorMultiTarget.py b/freqtrade/freqai/prediction_models/XGBoostRegressorMultiTarget.py index 38c478c0b..920745ec9 100644 --- a/freqtrade/freqai/prediction_models/XGBoostRegressorMultiTarget.py +++ b/freqtrade/freqai/prediction_models/XGBoostRegressorMultiTarget.py @@ -55,6 +55,9 @@ class XGBoostRegressorMultiTarget(BaseRegressionModel): 'xgb_model': init_models[i]}) model = FreqaiMultiOutputRegressor(estimator=xgb) + thread_training = self.freqai_info.get('multitarget_parallel_training', False) + if thread_training: + model.n_jobs = y.shape[1] model.fit(X=X, y=y, sample_weight=sample_weight, fit_params=fit_params) return model