From 714d9534b65edd4103fef7c2d5a9ba3700608b47 Mon Sep 17 00:00:00 2001 From: robcaulk Date: Tue, 19 Jul 2022 16:16:44 +0200 Subject: [PATCH 01/23] start adding tests --- freqtrade/freqai/data_kitchen.py | 36 ++++-------- tests/freqai/conftest.py | 68 +++++++++++++++++++++++ tests/freqai/test_freqai.py | 95 ++++++++++++++++++++++++++++++++ 3 files changed, 175 insertions(+), 24 deletions(-) create mode 100644 tests/freqai/conftest.py create mode 100644 tests/freqai/test_freqai.py diff --git a/freqtrade/freqai/data_kitchen.py b/freqtrade/freqai/data_kitchen.py index f320bdc2f..02b121134 100644 --- a/freqtrade/freqai/data_kitchen.py +++ b/freqtrade/freqai/data_kitchen.py @@ -410,6 +410,11 @@ class FreqaiDataKitchen: bt_split: the backtesting length (dats). Specified in user configuration file """ + if not isinstance(train_split, int) or train_split < 1: + raise OperationalException( + "train_period_days must be an integer greater than 0. " + f"Got {train_split}." + ) train_period_days = train_split * SECONDS_IN_DAY bt_period = bt_split * SECONDS_IN_DAY @@ -742,6 +747,13 @@ class FreqaiDataKitchen: return def create_fulltimerange(self, backtest_tr: str, backtest_period_days: int) -> str: + + if not isinstance(backtest_period_days, int): + raise OperationalException('backtest_period_days must be an integer') + + if backtest_period_days < 0: + raise OperationalException('backtest_period_days must be positive') + backtest_timerange = TimeRange.parse_timerange(backtest_tr) if backtest_timerange.stopts == 0: @@ -869,30 +881,6 @@ class FreqaiDataKitchen: self.model_filename = "cb_" + coin.lower() + "_" + str(int(trained_timerange.stopts)) - # self.freqai_config['live_trained_timerange'] = str(int(trained_timerange.stopts)) - # enables persistence, but not fully implemented into save/load data yer - # self.data['live_trained_timerange'] = str(int(trained_timerange.stopts)) - - # SUPERCEDED - # def download_new_data_for_retraining(self, timerange: TimeRange, metadata: dict, - # strategy: IStrategy) -> None: - - # exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], - # self.config, validate=False, freqai=True) - # # exchange = strategy.dp._exchange # closes ccxt session - # pairs = copy.deepcopy(self.freqai_config.get('corr_pairlist', [])) - # if str(metadata['pair']) not in pairs: - # pairs.append(str(metadata['pair'])) - - # refresh_backtest_ohlcv_data( - # exchange, pairs=pairs, timeframes=self.freqai_config.get('timeframes'), - # datadir=self.config['datadir'], timerange=timerange, - # new_pairs_days=self.config['new_pairs_days'], - # erase=False, data_format=self.config.get('dataformat_ohlcv', 'json'), - # trading_mode=self.config.get('trading_mode', 'spot'), - # prepend=self.config.get('prepend_data', False) - # ) - def download_all_data_for_training(self, timerange: TimeRange) -> None: """ Called only once upon start of bot to download the necessary data for diff --git a/tests/freqai/conftest.py b/tests/freqai/conftest.py new file mode 100644 index 000000000..cfc23939e --- /dev/null +++ b/tests/freqai/conftest.py @@ -0,0 +1,68 @@ +from copy import deepcopy +from pathlib import Path +from unittest.mock import MagicMock + +from freqtrade.freqai.data_kitchen import FreqaiDataKitchen +from freqtrade.resolvers import StrategyResolver +from freqtrade.resolvers.freqaimodel_resolver import FreqaiModelResolver + + +# @pytest.fixture(scope="function") +def freqai_conf(default_conf): + freqaiconf = deepcopy(default_conf) + freqaiconf.update( + { + "datadir": Path(default_conf["datadir"]), + "strategy": "FreqaiExampleStrategy", + "strategy-path": "freqtrade/templates", + "freqaimodel": "LightGBMPredictionModel", + "freqaimodel_path": "freqai/prediction_models", + "timerange": "20180110-20180115", + "freqai": { + "startup_candles": 10000, + "purge_old_models": True, + "train_period_days": 15, + "backtest_period_days": 7, + "live_retrain_hours": 0, + "identifier": "uniqe-id7", + "live_trained_timestamp": 0, + "feature_parameters": { + "include_timeframes": ["5m"], + "include_corr_pairlist": ["ADA/BTC", "DASH/BTC"], + "label_period_candles": 20, + "include_shifted_candles": 2, + "DI_threshold": 0.9, + "weight_factor": 0.9, + "principal_component_analysis": False, + "use_SVM_to_remove_outliers": True, + "stratify_training_data": 0, + "indicator_max_period_candles": 10, + "indicator_periods_candles": [10], + }, + "data_split_parameters": {"test_size": 0.33, "random_state": 1}, + "model_training_parameters": {"n_estimators": 1000, "task_type": "CPU"}, + }, + "config_files": [Path('config_examples', 'config_freqai_futures.example.json')] + } + ) + freqaiconf['exchange'].update({'pair_whitelist': ['ADA/BTC', 'DASH/BTC', 'ETH/BTC', 'LTC/BTC']}) + return freqaiconf + + +def get_patched_data_kitchen(mocker, freqaiconf): + dd = mocker.patch('freqtrade.freqai.data_drawer', MagicMock()) + dk = FreqaiDataKitchen(freqaiconf, dd) + return dk + + +def get_patched_strategy(mocker, freqaiconf): + strategy = StrategyResolver.load_strategy(freqaiconf) + strategy.bot_start() + + return strategy + + +def get_patched_freqaimodel(mocker, freqaiconf): + freqaimodel = FreqaiModelResolver.load_freqaimodel(freqaiconf) + + return freqaimodel diff --git a/tests/freqai/test_freqai.py b/tests/freqai/test_freqai.py new file mode 100644 index 000000000..185e55744 --- /dev/null +++ b/tests/freqai/test_freqai.py @@ -0,0 +1,95 @@ +# from unittest.mock import MagicMock +# from freqtrade.commands.optimize_commands import setup_optimize_configuration, start_edge +import copy + +import pytest + +from freqtrade.configuration import TimeRange +from freqtrade.data.dataprovider import DataProvider +# from freqtrade.freqai.data_drawer import FreqaiDataDrawer +from freqtrade.exceptions import OperationalException +from freqtrade.freqai.data_kitchen import FreqaiDataKitchen +from tests.conftest import get_patched_exchange +from tests.freqai.conftest import freqai_conf, get_patched_data_kitchen, get_patched_strategy + + +@pytest.mark.parametrize( + "timerange, train_period_days, expected_result", + [ + ("20220101-20220201", 30, "20211202-20220201"), + ("20220301-20220401", 15, "20220214-20220401"), + ], +) +def test_create_fulltimerange( + timerange, train_period_days, expected_result, default_conf, mocker, caplog +): + dk = get_patched_data_kitchen(mocker, freqai_conf(copy.deepcopy(default_conf))) + assert dk.create_fulltimerange(timerange, train_period_days) == expected_result + + +def test_create_fulltimerange_incorrect_backtest_period(mocker, default_conf): + dk = get_patched_data_kitchen(mocker, freqai_conf(copy.deepcopy(default_conf))) + with pytest.raises(OperationalException, match=r"backtest_period_days must be an integer"): + dk.create_fulltimerange("20220101-20220201", 0.5) + with pytest.raises(OperationalException, match=r"backtest_period_days must be positive"): + dk.create_fulltimerange("20220101-20220201", -1) + + +def test_split_timerange(mocker, default_conf): + freqaiconf = freqai_conf(copy.deepcopy(default_conf)) + freqaiconf.update({"timerange": "20220101-20220401"}) + dk = get_patched_data_kitchen(mocker, freqaiconf) + tr_list, bt_list = dk.split_timerange("20220101-20220201", 30, 7) + assert len(tr_list) == len(bt_list) == 9 + + tr_list, bt_list = dk.split_timerange("20220101-20220201", 30, 0.5) + assert len(tr_list) == len(bt_list) == 120 + + tr_list, bt_list = dk.split_timerange("20220101-20220201", 10, 1) + assert len(tr_list) == len(bt_list) == 80 + + with pytest.raises( + OperationalException, match=r"train_period_days must be an integer greater than 0." + ): + dk.split_timerange("20220101-20220201", -1, 0.5) + + +def test_update_historic_data(mocker, default_conf): + freqaiconf = freqai_conf(copy.deepcopy(default_conf)) + strategy = get_patched_strategy(mocker, freqaiconf) + exchange = get_patched_exchange(mocker, freqaiconf) + strategy.dp = DataProvider(freqaiconf, exchange) + freqai = strategy.model.bridge + freqai.live = True + freqai.dk = FreqaiDataKitchen(freqaiconf, freqai.dd) + timerange = TimeRange.parse_timerange("20180110-20180114") + + freqai.dk.load_all_pair_histories(timerange) + historic_candles = len(freqai.dd.historic_data["ADA/BTC"]["5m"]) + dp_candles = len(strategy.dp.get_pair_dataframe("ADA/BTC", "5m")) + candle_difference = dp_candles - historic_candles + freqai.dk.update_historic_data(strategy) + + updated_historic_candles = len(freqai.dd.historic_data["ADA/BTC"]["5m"]) + + assert updated_historic_candles - historic_candles == candle_difference + + +# def generate_test_data(timeframe: str, size: int, start: str = '2020-07-05'): +# np.random.seed(42) +# tf_mins = timeframe_to_minutes(timeframe) + +# base = np.random.normal(20, 2, size=size) + +# date = pd.date_range(start, periods=size, freq=f'{tf_mins}min', tz='UTC') +# df = pd.DataFrame({ +# 'date': date, +# 'open': base, +# 'high': base + np.random.normal(2, 1, size=size), +# 'low': base - np.random.normal(2, 1, size=size), +# 'close': base + np.random.normal(0, 1, size=size), +# 'volume': np.random.normal(200, size=size) +# } +# ) +# df = df.dropna() +# return df From 9c051958a6fcef8ce2055ce984e82363fe00639d Mon Sep 17 00:00:00 2001 From: lolong Date: Tue, 19 Jul 2022 17:49:18 +0200 Subject: [PATCH 02/23] Feat/freqai (#7105) Vectorize weight setting, log training dates Co-authored-by: robcaulk --- freqtrade/freqai/data_kitchen.py | 9 +++------ .../freqai/prediction_models/BaseRegressionModel.py | 6 +++++- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/freqtrade/freqai/data_kitchen.py b/freqtrade/freqai/data_kitchen.py index 02b121134..5c05d94e0 100644 --- a/freqtrade/freqai/data_kitchen.py +++ b/freqtrade/freqai/data_kitchen.py @@ -680,12 +680,9 @@ class FreqaiDataKitchen: Set weights so that recent data is more heavily weighted during training than older data. """ - - weights = np.zeros(num_weights) - for i in range(1, len(weights)): - weights[len(weights) - i] = np.exp( - -i / (self.config["freqai"]["feature_parameters"]["weight_factor"] * num_weights) - ) + wfactor = self.config["freqai"]["feature_parameters"]["weight_factor"] + weights = np.exp( + - np.arange(num_weights) / (wfactor * num_weights))[::-1] return weights def append_predictions(self, predictions, do_predict, len_dataframe): diff --git a/freqtrade/freqai/prediction_models/BaseRegressionModel.py b/freqtrade/freqai/prediction_models/BaseRegressionModel.py index f9a9bb69f..ffe30ef2a 100644 --- a/freqtrade/freqai/prediction_models/BaseRegressionModel.py +++ b/freqtrade/freqai/prediction_models/BaseRegressionModel.py @@ -39,7 +39,7 @@ class BaseRegressionModel(IFreqaiModel): :model: Trained model which can be used to inference (self.predict) """ - logger.info("--------------------Starting training " f"{pair} --------------------") + logger.info("-------------------- Starting training " f"{pair} --------------------") # filter the features requested by user in the configuration file and elegantly handle NaNs features_filtered, labels_filtered = dk.filter_features( @@ -49,6 +49,10 @@ class BaseRegressionModel(IFreqaiModel): training_filter=True, ) + start_date = unfiltered_dataframe["date"].iloc[0].strftime("%Y-%m-%d") + end_date = unfiltered_dataframe["date"].iloc[-1].strftime("%Y-%m-%d") + logger.info(f"-------------------- Training on data from {start_date} to " + f"{end_date}--------------------") # split data into train/test data. data_dictionary = dk.make_train_test_datasets(features_filtered, labels_filtered) if not self.freqai_info.get('fit_live_predictions', 0): From d43c146676a8c5842b247d247c7537ad86905405 Mon Sep 17 00:00:00 2001 From: robcaulk Date: Wed, 20 Jul 2022 12:56:46 +0200 Subject: [PATCH 03/23] add more tests for datakitchen functionalities, add regression tests for freqai_interface train/backtest --- freqtrade/freqai/data_kitchen.py | 15 -- freqtrade/freqai/freqai_interface.py | 3 +- setup.sh | 10 +- tests/freqai/conftest.py | 61 +++++++- tests/freqai/test_freqai.py | 95 ------------ tests/freqai/test_freqai_datakitchen.py | 167 +++++++++++++++++++++ tests/freqai/test_freqai_interface.py | 183 ++++++++++++++++++++++++ 7 files changed, 415 insertions(+), 119 deletions(-) delete mode 100644 tests/freqai/test_freqai.py create mode 100644 tests/freqai/test_freqai_datakitchen.py create mode 100644 tests/freqai/test_freqai_interface.py diff --git a/freqtrade/freqai/data_kitchen.py b/freqtrade/freqai/data_kitchen.py index 5c05d94e0..0966f8421 100644 --- a/freqtrade/freqai/data_kitchen.py +++ b/freqtrade/freqai/data_kitchen.py @@ -690,8 +690,6 @@ class FreqaiDataKitchen: Append backtest prediction from current backtest period to all previous periods """ - # ones = np.ones(len(predictions)) - # target_mean, target_std = ones * self.data["target_mean"], ones * self.data["target_std"] self.append_df = DataFrame() for label in self.label_list: self.append_df[label] = predictions[label] @@ -707,13 +705,6 @@ class FreqaiDataKitchen: else: self.full_df = pd.concat([self.full_df, self.append_df], axis=0) - # self.full_predictions = np.append(self.full_predictions, predictions) - # self.full_do_predict = np.append(self.full_do_predict, do_predict) - # if self.freqai_config.get("feature_parameters", {}).get("DI_threshold", 0) > 0: - # self.full_DI_values = np.append(self.full_DI_values, self.DI_values) - # self.full_target_mean = np.append(self.full_target_mean, target_mean) - # self.full_target_std = np.append(self.full_target_std, target_std) - return def fill_predictions(self, dataframe): @@ -734,12 +725,6 @@ class FreqaiDataKitchen: self.append_df = DataFrame() self.full_df = DataFrame() - # self.full_predictions = np.append(filler, self.full_predictions) - # self.full_do_predict = np.append(filler, self.full_do_predict) - # if self.freqai_config.get("feature_parameters", {}).get("DI_threshold", 0) > 0: - # self.full_DI_values = np.append(filler, self.full_DI_values) - # self.full_target_mean = np.append(filler, self.full_target_mean) - # self.full_target_std = np.append(filler, self.full_target_std) return diff --git a/freqtrade/freqai/freqai_interface.py b/freqtrade/freqai/freqai_interface.py index 56a179dc3..5acfcb9cf 100644 --- a/freqtrade/freqai/freqai_interface.py +++ b/freqtrade/freqai/freqai_interface.py @@ -170,11 +170,10 @@ class IFreqaiModel(ABC): gc.collect() dk.data = {} # clean the pair specific data between training window sliding self.training_timerange = tr_train - # self.training_timerange_timerange = tr_train dataframe_train = dk.slice_dataframe(tr_train, dataframe) dataframe_backtest = dk.slice_dataframe(tr_backtest, dataframe) - trained_timestamp = tr_train # TimeRange.parse_timerange(tr_train) + trained_timestamp = tr_train tr_train_startts_str = datetime.datetime.utcfromtimestamp(tr_train.startts).strftime( "%Y-%m-%d %H:%M:%S" ) diff --git a/setup.sh b/setup.sh index 202cb70c7..1a4a285a3 100755 --- a/setup.sh +++ b/setup.sh @@ -77,7 +77,15 @@ function updateenv() { fi fi - ${PYTHON} -m pip install --upgrade -r ${REQUIREMENTS} ${REQUIREMENTS_HYPEROPT} ${REQUIREMENTS_PLOT} + REQUIREMENTS_FREQAI="" + read -p "Do you want to install dependencies for freqai [y/N]? " + dev=$REPLY + if [[ $REPLY =~ ^[Yy]$ ]] + then + REQUIREMENTS_FREQAI="-r requirements-freqai.txt" + fi + + ${PYTHON} -m pip install --upgrade -r ${REQUIREMENTS} ${REQUIREMENTS_HYPEROPT} ${REQUIREMENTS_PLOT} ${REQUIREMENTS_FREQAI} if [ $? -ne 0 ]; then echo "Failed installing dependencies" exit 1 diff --git a/tests/freqai/conftest.py b/tests/freqai/conftest.py index cfc23939e..d9f4cc635 100644 --- a/tests/freqai/conftest.py +++ b/tests/freqai/conftest.py @@ -2,9 +2,12 @@ from copy import deepcopy from pathlib import Path from unittest.mock import MagicMock +from freqtrade.configuration import TimeRange +from freqtrade.data.dataprovider import DataProvider from freqtrade.freqai.data_kitchen import FreqaiDataKitchen from freqtrade.resolvers import StrategyResolver from freqtrade.resolvers.freqaimodel_resolver import FreqaiModelResolver +from tests.conftest import get_patched_exchange # @pytest.fixture(scope="function") @@ -21,16 +24,17 @@ def freqai_conf(default_conf): "freqai": { "startup_candles": 10000, "purge_old_models": True, - "train_period_days": 15, - "backtest_period_days": 7, + "train_period_days": 5, + "backtest_period_days": 2, "live_retrain_hours": 0, - "identifier": "uniqe-id7", + "expiration_hours": 1, + "identifier": "uniqe-id100", "live_trained_timestamp": 0, "feature_parameters": { "include_timeframes": ["5m"], "include_corr_pairlist": ["ADA/BTC", "DASH/BTC"], "label_period_candles": 20, - "include_shifted_candles": 2, + "include_shifted_candles": 1, "DI_threshold": 0.9, "weight_factor": 0.9, "principal_component_analysis": False, @@ -40,7 +44,7 @@ def freqai_conf(default_conf): "indicator_periods_candles": [10], }, "data_split_parameters": {"test_size": 0.33, "random_state": 1}, - "model_training_parameters": {"n_estimators": 1000, "task_type": "CPU"}, + "model_training_parameters": {"n_estimators": 100}, }, "config_files": [Path('config_examples', 'config_freqai_futures.example.json')] } @@ -55,7 +59,7 @@ def get_patched_data_kitchen(mocker, freqaiconf): return dk -def get_patched_strategy(mocker, freqaiconf): +def get_patched_freqai_strategy(mocker, freqaiconf): strategy = StrategyResolver.load_strategy(freqaiconf) strategy.bot_start() @@ -66,3 +70,48 @@ def get_patched_freqaimodel(mocker, freqaiconf): freqaimodel = FreqaiModelResolver.load_freqaimodel(freqaiconf) return freqaimodel + + +def get_freqai_live_analyzed_dataframe(mocker, freqaiconf): + strategy = get_patched_freqai_strategy(mocker, freqaiconf) + exchange = get_patched_exchange(mocker, freqaiconf) + strategy.dp = DataProvider(freqaiconf, exchange) + freqai = strategy.model.bridge + freqai.live = True + freqai.dk = FreqaiDataKitchen(freqaiconf, freqai.dd) + timerange = TimeRange.parse_timerange("20180110-20180114") + freqai.dk.load_all_pair_histories(timerange) + + strategy.analyze_pair('ADA/BTC', '5m') + return strategy.dp.get_analyzed_dataframe('ADA/BTC', '5m') + + +def get_freqai_analyzed_dataframe(mocker, freqaiconf): + strategy = get_patched_freqai_strategy(mocker, freqaiconf) + exchange = get_patched_exchange(mocker, freqaiconf) + strategy.dp = DataProvider(freqaiconf, exchange) + strategy.freqai_info = freqaiconf.get("freqai", {}) + freqai = strategy.model.bridge + freqai.live = True + freqai.dk = FreqaiDataKitchen(freqaiconf, freqai.dd) + timerange = TimeRange.parse_timerange("20180110-20180114") + freqai.dk.load_all_pair_histories(timerange) + sub_timerange = TimeRange.parse_timerange("20180111-20180114") + corr_df, base_df = freqai.dk.get_base_and_corr_dataframes(sub_timerange, "LTC/BTC") + + return freqai.dk.use_strategy_to_populate_indicators(strategy, corr_df, base_df, 'LTC/BTC') + + +def get_ready_to_train(mocker, freqaiconf): + strategy = get_patched_freqai_strategy(mocker, freqaiconf) + exchange = get_patched_exchange(mocker, freqaiconf) + strategy.dp = DataProvider(freqaiconf, exchange) + strategy.freqai_info = freqaiconf.get("freqai", {}) + freqai = strategy.model.bridge + freqai.live = True + freqai.dk = FreqaiDataKitchen(freqaiconf, freqai.dd) + timerange = TimeRange.parse_timerange("20180110-20180114") + freqai.dk.load_all_pair_histories(timerange) + sub_timerange = TimeRange.parse_timerange("20180111-20180114") + corr_df, base_df = freqai.dk.get_base_and_corr_dataframes(sub_timerange, "LTC/BTC") + return corr_df, base_df, freqai, strategy diff --git a/tests/freqai/test_freqai.py b/tests/freqai/test_freqai.py deleted file mode 100644 index 185e55744..000000000 --- a/tests/freqai/test_freqai.py +++ /dev/null @@ -1,95 +0,0 @@ -# from unittest.mock import MagicMock -# from freqtrade.commands.optimize_commands import setup_optimize_configuration, start_edge -import copy - -import pytest - -from freqtrade.configuration import TimeRange -from freqtrade.data.dataprovider import DataProvider -# from freqtrade.freqai.data_drawer import FreqaiDataDrawer -from freqtrade.exceptions import OperationalException -from freqtrade.freqai.data_kitchen import FreqaiDataKitchen -from tests.conftest import get_patched_exchange -from tests.freqai.conftest import freqai_conf, get_patched_data_kitchen, get_patched_strategy - - -@pytest.mark.parametrize( - "timerange, train_period_days, expected_result", - [ - ("20220101-20220201", 30, "20211202-20220201"), - ("20220301-20220401", 15, "20220214-20220401"), - ], -) -def test_create_fulltimerange( - timerange, train_period_days, expected_result, default_conf, mocker, caplog -): - dk = get_patched_data_kitchen(mocker, freqai_conf(copy.deepcopy(default_conf))) - assert dk.create_fulltimerange(timerange, train_period_days) == expected_result - - -def test_create_fulltimerange_incorrect_backtest_period(mocker, default_conf): - dk = get_patched_data_kitchen(mocker, freqai_conf(copy.deepcopy(default_conf))) - with pytest.raises(OperationalException, match=r"backtest_period_days must be an integer"): - dk.create_fulltimerange("20220101-20220201", 0.5) - with pytest.raises(OperationalException, match=r"backtest_period_days must be positive"): - dk.create_fulltimerange("20220101-20220201", -1) - - -def test_split_timerange(mocker, default_conf): - freqaiconf = freqai_conf(copy.deepcopy(default_conf)) - freqaiconf.update({"timerange": "20220101-20220401"}) - dk = get_patched_data_kitchen(mocker, freqaiconf) - tr_list, bt_list = dk.split_timerange("20220101-20220201", 30, 7) - assert len(tr_list) == len(bt_list) == 9 - - tr_list, bt_list = dk.split_timerange("20220101-20220201", 30, 0.5) - assert len(tr_list) == len(bt_list) == 120 - - tr_list, bt_list = dk.split_timerange("20220101-20220201", 10, 1) - assert len(tr_list) == len(bt_list) == 80 - - with pytest.raises( - OperationalException, match=r"train_period_days must be an integer greater than 0." - ): - dk.split_timerange("20220101-20220201", -1, 0.5) - - -def test_update_historic_data(mocker, default_conf): - freqaiconf = freqai_conf(copy.deepcopy(default_conf)) - strategy = get_patched_strategy(mocker, freqaiconf) - exchange = get_patched_exchange(mocker, freqaiconf) - strategy.dp = DataProvider(freqaiconf, exchange) - freqai = strategy.model.bridge - freqai.live = True - freqai.dk = FreqaiDataKitchen(freqaiconf, freqai.dd) - timerange = TimeRange.parse_timerange("20180110-20180114") - - freqai.dk.load_all_pair_histories(timerange) - historic_candles = len(freqai.dd.historic_data["ADA/BTC"]["5m"]) - dp_candles = len(strategy.dp.get_pair_dataframe("ADA/BTC", "5m")) - candle_difference = dp_candles - historic_candles - freqai.dk.update_historic_data(strategy) - - updated_historic_candles = len(freqai.dd.historic_data["ADA/BTC"]["5m"]) - - assert updated_historic_candles - historic_candles == candle_difference - - -# def generate_test_data(timeframe: str, size: int, start: str = '2020-07-05'): -# np.random.seed(42) -# tf_mins = timeframe_to_minutes(timeframe) - -# base = np.random.normal(20, 2, size=size) - -# date = pd.date_range(start, periods=size, freq=f'{tf_mins}min', tz='UTC') -# df = pd.DataFrame({ -# 'date': date, -# 'open': base, -# 'high': base + np.random.normal(2, 1, size=size), -# 'low': base - np.random.normal(2, 1, size=size), -# 'close': base + np.random.normal(0, 1, size=size), -# 'volume': np.random.normal(200, size=size) -# } -# ) -# df = df.dropna() -# return df diff --git a/tests/freqai/test_freqai_datakitchen.py b/tests/freqai/test_freqai_datakitchen.py new file mode 100644 index 000000000..49374b257 --- /dev/null +++ b/tests/freqai/test_freqai_datakitchen.py @@ -0,0 +1,167 @@ +# from unittest.mock import MagicMock +# from freqtrade.commands.optimize_commands import setup_optimize_configuration, start_edge +import copy +import datetime +import shutil +from pathlib import Path + +import pytest + +from freqtrade.configuration import TimeRange +from freqtrade.data.dataprovider import DataProvider +# from freqtrade.freqai.data_drawer import FreqaiDataDrawer +from freqtrade.exceptions import OperationalException +from freqtrade.freqai.data_kitchen import FreqaiDataKitchen +from tests.conftest import get_patched_exchange +from tests.freqai.conftest import freqai_conf, get_patched_data_kitchen, get_patched_freqai_strategy + + +@pytest.mark.parametrize( + "timerange, train_period_days, expected_result", + [ + ("20220101-20220201", 30, "20211202-20220201"), + ("20220301-20220401", 15, "20220214-20220401"), + ], +) +def test_create_fulltimerange( + timerange, train_period_days, expected_result, default_conf, mocker, caplog +): + dk = get_patched_data_kitchen(mocker, freqai_conf(copy.deepcopy(default_conf))) + assert dk.create_fulltimerange(timerange, train_period_days) == expected_result + shutil.rmtree(Path(dk.full_path)) + + +def test_create_fulltimerange_incorrect_backtest_period(mocker, default_conf): + dk = get_patched_data_kitchen(mocker, freqai_conf(copy.deepcopy(default_conf))) + with pytest.raises(OperationalException, match=r"backtest_period_days must be an integer"): + dk.create_fulltimerange("20220101-20220201", 0.5) + with pytest.raises(OperationalException, match=r"backtest_period_days must be positive"): + dk.create_fulltimerange("20220101-20220201", -1) + shutil.rmtree(Path(dk.full_path)) + + +@pytest.mark.parametrize( + "timerange, train_period_days, backtest_period_days, expected_result", + [ + ("20220101-20220201", 30, 7, 9), + ("20220101-20220201", 30, 0.5, 120), + ("20220101-20220201", 10, 1, 80), + ], +) +def test_split_timerange( + mocker, default_conf, timerange, train_period_days, backtest_period_days, expected_result +): + freqaiconf = freqai_conf(copy.deepcopy(default_conf)) + freqaiconf.update({"timerange": "20220101-20220401"}) + dk = get_patched_data_kitchen(mocker, freqaiconf) + tr_list, bt_list = dk.split_timerange(timerange, train_period_days, backtest_period_days) + assert len(tr_list) == len(bt_list) == expected_result + + with pytest.raises( + OperationalException, match=r"train_period_days must be an integer greater than 0." + ): + dk.split_timerange("20220101-20220201", -1, 0.5) + shutil.rmtree(Path(dk.full_path)) + + +def test_update_historic_data(mocker, default_conf): + freqaiconf = freqai_conf(copy.deepcopy(default_conf)) + strategy = get_patched_freqai_strategy(mocker, freqaiconf) + exchange = get_patched_exchange(mocker, freqaiconf) + strategy.dp = DataProvider(freqaiconf, exchange) + freqai = strategy.model.bridge + freqai.live = True + freqai.dk = FreqaiDataKitchen(freqaiconf, freqai.dd) + timerange = TimeRange.parse_timerange("20180110-20180114") + + freqai.dk.load_all_pair_histories(timerange) + historic_candles = len(freqai.dd.historic_data["ADA/BTC"]["5m"]) + dp_candles = len(strategy.dp.get_pair_dataframe("ADA/BTC", "5m")) + candle_difference = dp_candles - historic_candles + freqai.dk.update_historic_data(strategy) + + updated_historic_candles = len(freqai.dd.historic_data["ADA/BTC"]["5m"]) + + assert updated_historic_candles - historic_candles == candle_difference + shutil.rmtree(Path(freqai.dk.full_path)) + + +@pytest.mark.parametrize( + "timestamp, expected", + [ + (datetime.datetime.now(tz=datetime.timezone.utc).timestamp() - 7200, True), + (datetime.datetime.now(tz=datetime.timezone.utc).timestamp(), False), + ], +) +def test_check_if_model_expired(mocker, default_conf, timestamp, expected): + freqaiconf = freqai_conf(copy.deepcopy(default_conf)) + dk = get_patched_data_kitchen(mocker, freqaiconf) + assert dk.check_if_model_expired(timestamp) == expected + shutil.rmtree(Path(dk.full_path)) + + +def test_load_all_pairs_histories(mocker, default_conf): + freqaiconf = freqai_conf(copy.deepcopy(default_conf)) + strategy = get_patched_freqai_strategy(mocker, freqaiconf) + exchange = get_patched_exchange(mocker, freqaiconf) + strategy.dp = DataProvider(freqaiconf, exchange) + freqai = strategy.model.bridge + freqai.live = True + freqai.dk = FreqaiDataKitchen(freqaiconf, freqai.dd) + timerange = TimeRange.parse_timerange("20180110-20180114") + freqai.dk.load_all_pair_histories(timerange) + + assert len(freqai.dd.historic_data.keys()) == len( + freqaiconf.get("exchange", {}).get("pair_whitelist") + ) + assert len(freqai.dd.historic_data["ADA/BTC"]) == len( + freqaiconf.get("freqai", {}).get("feature_parameters", {}).get("include_timeframes") + ) + shutil.rmtree(Path(freqai.dk.full_path)) + + +def test_get_base_and_corr_dataframes(mocker, default_conf): + freqaiconf = freqai_conf(copy.deepcopy(default_conf)) + strategy = get_patched_freqai_strategy(mocker, freqaiconf) + exchange = get_patched_exchange(mocker, freqaiconf) + strategy.dp = DataProvider(freqaiconf, exchange) + freqai = strategy.model.bridge + freqai.live = True + freqai.dk = FreqaiDataKitchen(freqaiconf, freqai.dd) + timerange = TimeRange.parse_timerange("20180110-20180114") + freqai.dk.load_all_pair_histories(timerange) + sub_timerange = TimeRange.parse_timerange("20180111-20180114") + corr_df, base_df = freqai.dk.get_base_and_corr_dataframes(sub_timerange, "LTC/BTC") + + num_tfs = len( + freqaiconf.get("freqai", {}).get("feature_parameters", {}).get("include_timeframes") + ) + + assert len(base_df.keys()) == num_tfs + + assert len(corr_df.keys()) == len( + freqaiconf.get("freqai", {}).get("feature_parameters", {}).get("include_corr_pairlist") + ) + + assert len(corr_df["ADA/BTC"].keys()) == num_tfs + shutil.rmtree(Path(freqai.dk.full_path)) + + +def test_use_strategy_to_populate_indicators(mocker, default_conf): + freqaiconf = freqai_conf(copy.deepcopy(default_conf)) + strategy = get_patched_freqai_strategy(mocker, freqaiconf) + exchange = get_patched_exchange(mocker, freqaiconf) + strategy.dp = DataProvider(freqaiconf, exchange) + strategy.freqai_info = freqaiconf.get("freqai", {}) + freqai = strategy.model.bridge + freqai.live = True + freqai.dk = FreqaiDataKitchen(freqaiconf, freqai.dd) + timerange = TimeRange.parse_timerange("20180110-20180114") + freqai.dk.load_all_pair_histories(timerange) + sub_timerange = TimeRange.parse_timerange("20180111-20180114") + corr_df, base_df = freqai.dk.get_base_and_corr_dataframes(sub_timerange, "LTC/BTC") + + df = freqai.dk.use_strategy_to_populate_indicators(strategy, corr_df, base_df, 'LTC/BTC') + + assert len(df.columns) == 90 + shutil.rmtree(Path(freqai.dk.full_path)) diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py new file mode 100644 index 000000000..46ce19f8c --- /dev/null +++ b/tests/freqai/test_freqai_interface.py @@ -0,0 +1,183 @@ +# from unittest.mock import MagicMock +# from freqtrade.commands.optimize_commands import setup_optimize_configuration, start_edge +import copy +import platform +import shutil +from pathlib import Path +from unittest.mock import MagicMock + +from freqtrade.configuration import TimeRange +from freqtrade.data.dataprovider import DataProvider +# from freqtrade.freqai.data_drawer import FreqaiDataDrawer +from freqtrade.freqai.data_kitchen import FreqaiDataKitchen +from tests.conftest import get_patched_exchange, log_has +from tests.freqai.conftest import freqai_conf, get_patched_freqai_strategy + + +def test_train_model_in_series_LightGBM(mocker, default_conf): + freqaiconf = freqai_conf(copy.deepcopy(default_conf)) + freqaiconf.update({"timerange": "20180110-20180130"}) + + strategy = get_patched_freqai_strategy(mocker, freqaiconf) + exchange = get_patched_exchange(mocker, freqaiconf) + strategy.dp = DataProvider(freqaiconf, exchange) + strategy.freqai_info = freqaiconf.get("freqai", {}) + freqai = strategy.model.bridge + freqai.live = True + freqai.dk = FreqaiDataKitchen(freqaiconf, freqai.dd) + timerange = TimeRange.parse_timerange("20180110-20180130") + freqai.dk.load_all_pair_histories(timerange) + + freqai.dd.pair_dict = MagicMock() + + data_load_timerange = TimeRange.parse_timerange("20180110-20180130") + new_timerange = TimeRange.parse_timerange("20180120-20180130") + + freqai.train_model_in_series(new_timerange, "ADA/BTC", strategy, freqai.dk, data_load_timerange) + + assert ( + Path(freqai.dk.data_path / str(freqai.dk.model_filename + "_model.joblib")) + .resolve() + .exists() + ) + assert ( + Path(freqai.dk.data_path / str(freqai.dk.model_filename + "_metadata.json")) + .resolve() + .exists() + ) + assert ( + Path(freqai.dk.data_path / str(freqai.dk.model_filename + "_trained_df.pkl")) + .resolve() + .exists() + ) + assert ( + Path(freqai.dk.data_path / str(freqai.dk.model_filename + "_svm_model.joblib")) + .resolve() + .exists() + ) + + shutil.rmtree(Path(freqai.dk.full_path)) + + +# Catboost not available for ARM architecture. using platform lib to check processor type +if "arm" not in platform.uname()[-1]: + + def test_train_model_in_series_Catboost(mocker, default_conf): + freqaiconf = freqai_conf(copy.deepcopy(default_conf)) + freqaiconf.update({"timerange": "20180110-20180130"}) + freqaiconf.update({"freqaimodel": "CatboostPredictionModel"}) + strategy = get_patched_freqai_strategy(mocker, freqaiconf) + exchange = get_patched_exchange(mocker, freqaiconf) + strategy.dp = DataProvider(freqaiconf, exchange) + strategy.freqai_info = freqaiconf.get("freqai", {}) + freqai = strategy.model.bridge + freqai.live = True + freqai.dk = FreqaiDataKitchen(freqaiconf, freqai.dd) + timerange = TimeRange.parse_timerange("20180110-20180130") + freqai.dk.load_all_pair_histories(timerange) + + freqai.dd.pair_dict = MagicMock() + + data_load_timerange = TimeRange.parse_timerange("20180110-20180130") + new_timerange = TimeRange.parse_timerange("20180120-20180130") + + freqai.train_model_in_series( + new_timerange, "ADA/BTC", strategy, freqai.dk, data_load_timerange + ) + + assert ( + Path(freqai.dk.data_path / str(freqai.dk.model_filename + "_model.joblib")) + .resolve() + .exists() + ) + assert ( + Path(freqai.dk.data_path / str(freqai.dk.model_filename + "_metadata.json")) + .resolve() + .exists() + ) + assert ( + Path(freqai.dk.data_path / str(freqai.dk.model_filename + "_trained_df.pkl")) + .resolve() + .exists() + ) + assert ( + Path(freqai.dk.data_path / str(freqai.dk.model_filename + "_svm_model.joblib")) + .resolve() + .exists() + ) + + shutil.rmtree(Path(freqai.dk.full_path)) + + +def test_start_backtesting(mocker, default_conf): + freqaiconf = freqai_conf(copy.deepcopy(default_conf)) + freqaiconf.update({"timerange": "20180120-20180130"}) + strategy = get_patched_freqai_strategy(mocker, freqaiconf) + exchange = get_patched_exchange(mocker, freqaiconf) + strategy.dp = DataProvider(freqaiconf, exchange) + strategy.freqai_info = freqaiconf.get("freqai", {}) + freqai = strategy.model.bridge + freqai.live = False + freqai.dk = FreqaiDataKitchen(freqaiconf, freqai.dd) + timerange = TimeRange.parse_timerange("20180110-20180130") + freqai.dk.load_all_pair_histories(timerange) + sub_timerange = TimeRange.parse_timerange("20180110-20180130") + corr_df, base_df = freqai.dk.get_base_and_corr_dataframes(sub_timerange, "LTC/BTC") + + df = freqai.dk.use_strategy_to_populate_indicators(strategy, corr_df, base_df, "LTC/BTC") + + metadata = {"pair": "ADA/BTC"} + freqai.start_backtesting(df, metadata, freqai.dk) + model_folders = [x for x in freqai.dd.full_path.iterdir() if x.is_dir()] + + assert len(model_folders) == 5 + + shutil.rmtree(Path(freqai.dk.full_path)) + + +def test_start_backtesting_from_existing_folder(mocker, default_conf, caplog): + freqaiconf = freqai_conf(copy.deepcopy(default_conf)) + freqaiconf.update({"timerange": "20180120-20180130"}) + strategy = get_patched_freqai_strategy(mocker, freqaiconf) + exchange = get_patched_exchange(mocker, freqaiconf) + strategy.dp = DataProvider(freqaiconf, exchange) + strategy.freqai_info = freqaiconf.get("freqai", {}) + freqai = strategy.model.bridge + freqai.live = False + freqai.dk = FreqaiDataKitchen(freqaiconf, freqai.dd) + timerange = TimeRange.parse_timerange("20180110-20180130") + freqai.dk.load_all_pair_histories(timerange) + sub_timerange = TimeRange.parse_timerange("20180110-20180130") + corr_df, base_df = freqai.dk.get_base_and_corr_dataframes(sub_timerange, "LTC/BTC") + + df = freqai.dk.use_strategy_to_populate_indicators(strategy, corr_df, base_df, "LTC/BTC") + + metadata = {"pair": "ADA/BTC"} + freqai.start_backtesting(df, metadata, freqai.dk) + model_folders = [x for x in freqai.dd.full_path.iterdir() if x.is_dir()] + + assert len(model_folders) == 5 + + # without deleting the exiting folder structure, re-run + + freqaiconf.update({"timerange": "20180120-20180130"}) + strategy = get_patched_freqai_strategy(mocker, freqaiconf) + exchange = get_patched_exchange(mocker, freqaiconf) + strategy.dp = DataProvider(freqaiconf, exchange) + strategy.freqai_info = freqaiconf.get("freqai", {}) + freqai = strategy.model.bridge + freqai.live = False + freqai.dk = FreqaiDataKitchen(freqaiconf, freqai.dd) + timerange = TimeRange.parse_timerange("20180110-20180130") + freqai.dk.load_all_pair_histories(timerange) + sub_timerange = TimeRange.parse_timerange("20180110-20180130") + corr_df, base_df = freqai.dk.get_base_and_corr_dataframes(sub_timerange, "LTC/BTC") + + df = freqai.dk.use_strategy_to_populate_indicators(strategy, corr_df, base_df, "LTC/BTC") + freqai.start_backtesting(df, metadata, freqai.dk) + assert log_has( + "Found model at user_data/models/uniqe-id100/sub-train-ADA1517097600/cb_ada_1517097600", + caplog, + ) + + shutil.rmtree(Path(freqai.dk.full_path)) From 88d769d80105b442e2759b85f1f9b8b040f5a759 Mon Sep 17 00:00:00 2001 From: robcaulk Date: Wed, 20 Jul 2022 14:18:06 +0200 Subject: [PATCH 04/23] comment out problematic catboost test --- tests/freqai/test_freqai_interface.py | 90 +++++++++++++-------------- 1 file changed, 45 insertions(+), 45 deletions(-) diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index 46ce19f8c..78f748f6a 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -1,19 +1,21 @@ # from unittest.mock import MagicMock # from freqtrade.commands.optimize_commands import setup_optimize_configuration, start_edge import copy -import platform +# import platform import shutil from pathlib import Path from unittest.mock import MagicMock from freqtrade.configuration import TimeRange from freqtrade.data.dataprovider import DataProvider -# from freqtrade.freqai.data_drawer import FreqaiDataDrawer from freqtrade.freqai.data_kitchen import FreqaiDataKitchen from tests.conftest import get_patched_exchange, log_has from tests.freqai.conftest import freqai_conf, get_patched_freqai_strategy +# import pytest + + def test_train_model_in_series_LightGBM(mocker, default_conf): freqaiconf = freqai_conf(copy.deepcopy(default_conf)) freqaiconf.update({"timerange": "20180110-20180130"}) @@ -59,54 +61,52 @@ def test_train_model_in_series_LightGBM(mocker, default_conf): shutil.rmtree(Path(freqai.dk.full_path)) -# Catboost not available for ARM architecture. using platform lib to check processor type -if "arm" not in platform.uname()[-1]: +# FIXME: hits segfault +# @pytest.mark.skipif("arm" in platform.uname()[-1], reason="no ARM..") +# def test_train_model_in_series_Catboost(mocker, default_conf): +# freqaiconf = freqai_conf(copy.deepcopy(default_conf)) +# freqaiconf.update({"timerange": "20180110-20180130"}) +# freqaiconf.update({"freqaimodel": "CatboostPredictionModel"}) +# strategy = get_patched_freqai_strategy(mocker, freqaiconf) +# exchange = get_patched_exchange(mocker, freqaiconf) +# strategy.dp = DataProvider(freqaiconf, exchange) +# strategy.freqai_info = freqaiconf.get("freqai", {}) +# freqai = strategy.model.bridge +# freqai.live = True +# freqai.dk = FreqaiDataKitchen(freqaiconf, freqai.dd) +# timerange = TimeRange.parse_timerange("20180110-20180130") +# freqai.dk.load_all_pair_histories(timerange) - def test_train_model_in_series_Catboost(mocker, default_conf): - freqaiconf = freqai_conf(copy.deepcopy(default_conf)) - freqaiconf.update({"timerange": "20180110-20180130"}) - freqaiconf.update({"freqaimodel": "CatboostPredictionModel"}) - strategy = get_patched_freqai_strategy(mocker, freqaiconf) - exchange = get_patched_exchange(mocker, freqaiconf) - strategy.dp = DataProvider(freqaiconf, exchange) - strategy.freqai_info = freqaiconf.get("freqai", {}) - freqai = strategy.model.bridge - freqai.live = True - freqai.dk = FreqaiDataKitchen(freqaiconf, freqai.dd) - timerange = TimeRange.parse_timerange("20180110-20180130") - freqai.dk.load_all_pair_histories(timerange) +# freqai.dd.pair_dict = MagicMock() - freqai.dd.pair_dict = MagicMock() +# data_load_timerange = TimeRange.parse_timerange("20180110-20180130") +# new_timerange = TimeRange.parse_timerange("20180120-20180130") - data_load_timerange = TimeRange.parse_timerange("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.train_model_in_series( - new_timerange, "ADA/BTC", strategy, freqai.dk, data_load_timerange - ) +# assert ( +# Path(freqai.dk.data_path / str(freqai.dk.model_filename + "_model.joblib")) +# .resolve() +# .exists() +# ) +# assert ( +# Path(freqai.dk.data_path / str(freqai.dk.model_filename + "_metadata.json")) +# .resolve() +# .exists() +# ) +# assert ( +# Path(freqai.dk.data_path / str(freqai.dk.model_filename + "_trained_df.pkl")) +# .resolve() +# .exists() +# ) +# assert ( +# Path(freqai.dk.data_path / str(freqai.dk.model_filename + "_svm_model.joblib")) +# .resolve() +# .exists() +# ) - assert ( - Path(freqai.dk.data_path / str(freqai.dk.model_filename + "_model.joblib")) - .resolve() - .exists() - ) - assert ( - Path(freqai.dk.data_path / str(freqai.dk.model_filename + "_metadata.json")) - .resolve() - .exists() - ) - assert ( - Path(freqai.dk.data_path / str(freqai.dk.model_filename + "_trained_df.pkl")) - .resolve() - .exists() - ) - assert ( - Path(freqai.dk.data_path / str(freqai.dk.model_filename + "_svm_model.joblib")) - .resolve() - .exists() - ) - - shutil.rmtree(Path(freqai.dk.full_path)) +# shutil.rmtree(Path(freqai.dk.full_path)) def test_start_backtesting(mocker, default_conf): From c43935e82ad9b627875a61d02a2923ac101b7374 Mon Sep 17 00:00:00 2001 From: robcaulk Date: Wed, 20 Jul 2022 14:39:28 +0200 Subject: [PATCH 05/23] create dedicated minimal freqai test strat --- tests/freqai/conftest.py | 4 +- tests/freqai/test_freqai_datakitchen.py | 2 +- tests/strategy/strats/freqai_test_strat.py | 202 +++++++++++++++++++++ 3 files changed, 205 insertions(+), 3 deletions(-) create mode 100644 tests/strategy/strats/freqai_test_strat.py diff --git a/tests/freqai/conftest.py b/tests/freqai/conftest.py index d9f4cc635..f4af2c4a5 100644 --- a/tests/freqai/conftest.py +++ b/tests/freqai/conftest.py @@ -16,8 +16,8 @@ def freqai_conf(default_conf): freqaiconf.update( { "datadir": Path(default_conf["datadir"]), - "strategy": "FreqaiExampleStrategy", - "strategy-path": "freqtrade/templates", + "strategy": "freqai_test_strat", + "strategy-path": "freqtrade/tests/strategy/strats", "freqaimodel": "LightGBMPredictionModel", "freqaimodel_path": "freqai/prediction_models", "timerange": "20180110-20180115", diff --git a/tests/freqai/test_freqai_datakitchen.py b/tests/freqai/test_freqai_datakitchen.py index 49374b257..1964d1423 100644 --- a/tests/freqai/test_freqai_datakitchen.py +++ b/tests/freqai/test_freqai_datakitchen.py @@ -163,5 +163,5 @@ def test_use_strategy_to_populate_indicators(mocker, default_conf): df = freqai.dk.use_strategy_to_populate_indicators(strategy, corr_df, base_df, 'LTC/BTC') - assert len(df.columns) == 90 + assert len(df.columns) == 45 shutil.rmtree(Path(freqai.dk.full_path)) diff --git a/tests/strategy/strats/freqai_test_strat.py b/tests/strategy/strats/freqai_test_strat.py new file mode 100644 index 000000000..e2e823adb --- /dev/null +++ b/tests/strategy/strats/freqai_test_strat.py @@ -0,0 +1,202 @@ +import logging +from functools import reduce + +import pandas as pd +import talib.abstract as ta +from pandas import DataFrame + +from freqtrade.freqai.strategy_bridge import CustomModel +from freqtrade.strategy import DecimalParameter, IntParameter, merge_informative_pair +from freqtrade.strategy.interface import IStrategy + + +logger = logging.getLogger(__name__) + + +class freqai_test_strat(IStrategy): + """ + Example strategy showing how the user connects their own + IFreqaiModel to the strategy. Namely, the user uses: + self.model = CustomModel(self.config) + self.model.bridge.start(dataframe, metadata) + + to make predictions on their data. populate_any_indicators() automatically + generates the variety of features indicated by the user in the + canonical freqtrade configuration file under config['freqai']. + """ + + minimal_roi = {"0": 0.1, "240": -1} + + plot_config = { + "main_plot": {}, + "subplots": { + "prediction": {"prediction": {"color": "blue"}}, + "target_roi": { + "target_roi": {"color": "brown"}, + }, + "do_predict": { + "do_predict": {"color": "brown"}, + }, + }, + } + + process_only_new_candles = True + stoploss = -0.05 + use_exit_signal = True + startup_candle_count: int = 300 + can_short = False + + linear_roi_offset = DecimalParameter( + 0.00, 0.02, default=0.005, space="sell", optimize=False, load=True + ) + max_roi_time_long = IntParameter(0, 800, default=400, space="sell", optimize=False, load=True) + + def informative_pairs(self): + whitelist_pairs = self.dp.current_whitelist() + corr_pairs = self.config["freqai"]["feature_parameters"]["include_corr_pairlist"] + informative_pairs = [] + for tf in self.config["freqai"]["feature_parameters"]["include_timeframes"]: + for pair in whitelist_pairs: + informative_pairs.append((pair, tf)) + for pair in corr_pairs: + if pair in whitelist_pairs: + continue # avoid duplication + informative_pairs.append((pair, tf)) + return informative_pairs + + def bot_start(self): + self.model = CustomModel(self.config) + + def populate_any_indicators( + self, metadata, pair, df, tf, informative=None, coin="", set_generalized_indicators=False + ): + """ + Function designed to automatically generate, name and merge features + from user indicated timeframes in the configuration file. User controls the indicators + passed to the training/prediction by prepending indicators with `'%-' + coin ` + (see convention below). I.e. user should not prepend any supporting metrics + (e.g. bb_lowerband below) with % unless they explicitly want to pass that metric to the + model. + :params: + :pair: pair to be used as informative + :df: strategy dataframe which will receive merges from informatives + :tf: timeframe of the dataframe which will modify the feature names + :informative: the dataframe associated with the informative pair + :coin: the name of the coin which will modify the feature names. + """ + + with self.model.bridge.lock: + if informative is None: + informative = self.dp.get_pair_dataframe(pair, tf) + + # first loop is automatically duplicating indicators for time periods + for t in self.freqai_info["feature_parameters"]["indicator_periods_candles"]: + + t = int(t) + informative[f"%-{coin}rsi-period_{t}"] = ta.RSI(informative, timeperiod=t) + informative[f"%-{coin}mfi-period_{t}"] = ta.MFI(informative, timeperiod=t) + informative[f"%-{coin}adx-period_{t}"] = ta.ADX(informative, window=t) + + informative[f"%-{coin}pct-change"] = informative["close"].pct_change() + informative[f"%-{coin}raw_volume"] = informative["volume"] + informative[f"%-{coin}raw_price"] = informative["close"] + + indicators = [col for col in informative if col.startswith("%")] + # This loop duplicates and shifts all indicators to add a sense of recency to data + for n in range(self.freqai_info["feature_parameters"]["include_shifted_candles"] + 1): + if n == 0: + continue + informative_shift = informative[indicators].shift(n) + informative_shift = informative_shift.add_suffix("_shift-" + str(n)) + informative = pd.concat((informative, informative_shift), axis=1) + + df = merge_informative_pair(df, informative, self.config["timeframe"], tf, ffill=True) + skip_columns = [ + (s + "_" + tf) for s in ["date", "open", "high", "low", "close", "volume"] + ] + df = df.drop(columns=skip_columns) + + # Add generalized indicators here (because in live, it will call this + # function to populate indicators during training). Notice how we ensure not to + # add them multiple times + if set_generalized_indicators: + df["%-day_of_week"] = (df["date"].dt.dayofweek + 1) / 7 + df["%-hour_of_day"] = (df["date"].dt.hour + 1) / 25 + + # user adds targets here by prepending them with &- (see convention below) + # If user wishes to use multiple targets, a multioutput prediction model + # needs to be used such as templates/CatboostPredictionMultiModel.py + df["&-s_close"] = ( + df["close"] + .shift(-self.freqai_info["feature_parameters"]["label_period_candles"]) + .rolling(self.freqai_info["feature_parameters"]["label_period_candles"]) + .mean() + / df["close"] + - 1 + ) + + return df + + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + + self.freqai_info = self.config["freqai"] + self.pair = metadata["pair"] + sgi = True + # the following loops are necessary for building the features + # indicated by the user in the configuration file. + # All indicators must be populated by populate_any_indicators() for live functionality + # to work correctly. + for tf in self.freqai_info["feature_parameters"]["include_timeframes"]: + dataframe = self.populate_any_indicators( + metadata, + self.pair, + dataframe.copy(), + tf, + coin=self.pair.split("/")[0] + "-", + set_generalized_indicators=sgi, + ) + sgi = False + for pair in self.freqai_info["feature_parameters"]["include_corr_pairlist"]: + if metadata["pair"] in pair: + continue # do not include whitelisted pair twice if it is in corr_pairlist + dataframe = self.populate_any_indicators( + metadata, pair, dataframe.copy(), tf, coin=pair.split("/")[0] + "-" + ) + + # the model will return 4 values, its prediction, an indication of whether or not the + # prediction should be accepted, the target mean/std values from the labels used during + # each training period. + dataframe = self.model.bridge.start(dataframe, metadata, self) + + dataframe["target_roi"] = dataframe["&-s_close_mean"] + dataframe["&-s_close_std"] * 1.25 + dataframe["sell_roi"] = dataframe["&-s_close_mean"] - dataframe["&-s_close_std"] * 1.25 + return dataframe + + def populate_entry_trend(self, df: DataFrame, metadata: dict) -> DataFrame: + + enter_long_conditions = [df["do_predict"] == 1, df["&-s_close"] > df["target_roi"]] + + if enter_long_conditions: + df.loc[ + reduce(lambda x, y: x & y, enter_long_conditions), ["enter_long", "enter_tag"] + ] = (1, "long") + + enter_short_conditions = [df["do_predict"] == 1, df["&-s_close"] < df["sell_roi"]] + + if enter_short_conditions: + df.loc[ + reduce(lambda x, y: x & y, enter_short_conditions), ["enter_short", "enter_tag"] + ] = (1, "short") + + return df + + def populate_exit_trend(self, df: DataFrame, metadata: dict) -> DataFrame: + exit_long_conditions = [df["do_predict"] == 1, df["&-s_close"] < df["sell_roi"] * 0.25] + if exit_long_conditions: + df.loc[reduce(lambda x, y: x & y, exit_long_conditions), "exit_long"] = 1 + + exit_short_conditions = [df["do_predict"] == 1, df["&-s_close"] > df["target_roi"] * 0.25] + if exit_short_conditions: + df.loc[reduce(lambda x, y: x & y, exit_short_conditions), "exit_short"] = 1 + + return df From 286bd0c40baf056e01bc90b62712cbac5cf5dae4 Mon Sep 17 00:00:00 2001 From: robcaulk Date: Wed, 20 Jul 2022 15:00:02 +0200 Subject: [PATCH 06/23] follow string for adding a strat to tests/strategy/strats --- tests/rpc/test_rpc_apiserver.py | 3 ++- tests/strategy/test_strategy_loading.py | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 57c08f48e..86fe0bf03 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -1403,7 +1403,8 @@ def test_api_strategies(botclient): 'StrategyTestV2', 'StrategyTestV3', 'StrategyTestV3Analysis', - 'StrategyTestV3Futures' + 'StrategyTestV3Futures', + 'freqai_test_strat' ]} diff --git a/tests/strategy/test_strategy_loading.py b/tests/strategy/test_strategy_loading.py index bdfcf3211..aaad26e5b 100644 --- a/tests/strategy/test_strategy_loading.py +++ b/tests/strategy/test_strategy_loading.py @@ -34,7 +34,7 @@ def test_search_all_strategies_no_failed(): directory = Path(__file__).parent / "strats" strategies = StrategyResolver.search_all_objects(directory, enum_failed=False) assert isinstance(strategies, list) - assert len(strategies) == 7 + assert len(strategies) == 8 assert isinstance(strategies[0], dict) @@ -42,10 +42,10 @@ def test_search_all_strategies_with_failed(): directory = Path(__file__).parent / "strats" strategies = StrategyResolver.search_all_objects(directory, enum_failed=True) assert isinstance(strategies, list) - assert len(strategies) == 8 + assert len(strategies) == 9 # with enum_failed=True search_all_objects() shall find 2 good strategies # and 1 which fails to load - assert len([x for x in strategies if x['class'] is not None]) == 7 + assert len([x for x in strategies if x['class'] is not None]) == 8 assert len([x for x in strategies if x['class'] is None]) == 1 From 921a7ef21632d29300c6d31dd4f4d08065966578 Mon Sep 17 00:00:00 2001 From: robcaulk Date: Wed, 20 Jul 2022 15:51:25 +0200 Subject: [PATCH 07/23] add requirements-freqai.txt to builds --- .github/workflows/ci.yml | 4 ++-- build_helpers/install_windows.ps1 | 2 +- freqtrade/freqai/freqai_interface.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7b077be04..63598e316 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -60,7 +60,7 @@ jobs: export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH export TA_LIBRARY_PATH=${HOME}/dependencies/lib export TA_INCLUDE_PATH=${HOME}/dependencies/include - pip install -r requirements-dev.txt + pip install -r requirements-dev.txt -r requirements-freqai.txt pip install -e . - name: Tests @@ -159,7 +159,7 @@ jobs: export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH export TA_LIBRARY_PATH=${HOME}/dependencies/lib export TA_INCLUDE_PATH=${HOME}/dependencies/include - pip install -r requirements-dev.txt + pip install -r requirements-dev.txt -r requirements-freqai.txt pip install -e . - name: Tests diff --git a/build_helpers/install_windows.ps1 b/build_helpers/install_windows.ps1 index 4caefa340..846087b40 100644 --- a/build_helpers/install_windows.ps1 +++ b/build_helpers/install_windows.ps1 @@ -14,5 +14,5 @@ if ($pyv -eq '3.9') { if ($pyv -eq '3.10') { pip install build_helpers\TA_Lib-0.4.24-cp310-cp310-win_amd64.whl } -pip install -r requirements-dev.txt +pip install -r requirements-dev.txt -r requirements-freqai.txt pip install -e . diff --git a/freqtrade/freqai/freqai_interface.py b/freqtrade/freqai/freqai_interface.py index 5acfcb9cf..7ca43c0fe 100644 --- a/freqtrade/freqai/freqai_interface.py +++ b/freqtrade/freqai/freqai_interface.py @@ -180,8 +180,8 @@ class IFreqaiModel(ABC): tr_train_stopts_str = datetime.datetime.utcfromtimestamp(tr_train.stopts).strftime( "%Y-%m-%d %H:%M:%S" ) - logger.info("Training %s", metadata["pair"]) - logger.info(f"Training {tr_train_startts_str} to {tr_train_stopts_str}") + logger.info(f"Training {metadata['pair']}") + logger.info(f" from {tr_train_startts_str} to {tr_train_stopts_str}") dk.data_path = Path( dk.full_path From 4e5d60fdc91f994f0f3a32c479b2bffdff98b3dc Mon Sep 17 00:00:00 2001 From: robcaulk Date: Wed, 20 Jul 2022 15:54:22 +0200 Subject: [PATCH 08/23] match scikit-learn version to hyperopt required version --- requirements-freqai.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-freqai.txt b/requirements-freqai.txt index a06a41b96..c3aa2e4db 100644 --- a/requirements-freqai.txt +++ b/requirements-freqai.txt @@ -2,7 +2,7 @@ -r requirements.txt # Required for freqai -scikit-learn==1.0.2 +scikit-learn==1.1.1 scikit-optimize==0.9.0 joblib==1.1.0 catboost==1.0.4 From a99c126266974a5fcd51080233b744703656e172 Mon Sep 17 00:00:00 2001 From: robcaulk Date: Wed, 20 Jul 2022 16:14:19 +0200 Subject: [PATCH 09/23] help windows builds pass freqai tests. Add freqai to README.md --- README.md | 1 + tests/freqai/conftest.py | 2 +- tests/freqai/test_freqai_interface.py | 10 ++++------ 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 881895c9a..1c75652be 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,7 @@ Please find the complete documentation on the [freqtrade website](https://www.fr - [x] **Dry-run**: Run the bot without paying money. - [x] **Backtesting**: Run a simulation of your buy/sell strategy. - [x] **Strategy Optimization by machine learning**: Use machine learning to optimize your buy/sell strategy parameters with real exchange data. +- [X] **Adaptive prediction modeling**: Build a smart strategy with FreqAI that self-trains to the market via adaptive machine learning methods. [Learn more](https://www.freqtrade.io/en/stable/freqai/) - [x] **Edge position sizing** Calculate your win rate, risk reward ratio, the best stoploss and adjust your position size before taking a position for each specific market. [Learn more](https://www.freqtrade.io/en/stable/edge/). - [x] **Whitelist crypto-currencies**: Select which crypto-currency you want to trade or use dynamic whitelists. - [x] **Blacklist crypto-currencies**: Select which crypto-currency you want to avoid. diff --git a/tests/freqai/conftest.py b/tests/freqai/conftest.py index f4af2c4a5..549ba2663 100644 --- a/tests/freqai/conftest.py +++ b/tests/freqai/conftest.py @@ -44,7 +44,7 @@ def freqai_conf(default_conf): "indicator_periods_candles": [10], }, "data_split_parameters": {"test_size": 0.33, "random_state": 1}, - "model_training_parameters": {"n_estimators": 100}, + "model_training_parameters": {"n_estimators": 100, "verbosity": 0}, }, "config_files": [Path('config_examples', 'config_freqai_futures.example.json')] } diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index 78f748f6a..9219baee3 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -9,13 +9,10 @@ from unittest.mock import MagicMock from freqtrade.configuration import TimeRange from freqtrade.data.dataprovider import DataProvider from freqtrade.freqai.data_kitchen import FreqaiDataKitchen -from tests.conftest import get_patched_exchange, log_has +from tests.conftest import get_patched_exchange, log_has_re from tests.freqai.conftest import freqai_conf, get_patched_freqai_strategy -# import pytest - - def test_train_model_in_series_LightGBM(mocker, default_conf): freqaiconf = freqai_conf(copy.deepcopy(default_conf)) freqaiconf.update({"timerange": "20180110-20180130"}) @@ -175,8 +172,9 @@ def test_start_backtesting_from_existing_folder(mocker, default_conf, caplog): df = freqai.dk.use_strategy_to_populate_indicators(strategy, corr_df, base_df, "LTC/BTC") freqai.start_backtesting(df, metadata, freqai.dk) - assert log_has( - "Found model at user_data/models/uniqe-id100/sub-train-ADA1517097600/cb_ada_1517097600", + + assert log_has_re( + "Found model at ", caplog, ) From 6c5e48dd4fe1aabd3a0f76af5ae03b13b152b4de Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 21 Jul 2022 07:26:44 +0200 Subject: [PATCH 10/23] dev-dependencies should include freqAI --- .github/workflows/ci.yml | 4 ++-- build_helpers/install_windows.ps1 | 2 +- requirements-dev.txt | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 63598e316..7b077be04 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -60,7 +60,7 @@ jobs: export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH export TA_LIBRARY_PATH=${HOME}/dependencies/lib export TA_INCLUDE_PATH=${HOME}/dependencies/include - pip install -r requirements-dev.txt -r requirements-freqai.txt + pip install -r requirements-dev.txt pip install -e . - name: Tests @@ -159,7 +159,7 @@ jobs: export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH export TA_LIBRARY_PATH=${HOME}/dependencies/lib export TA_INCLUDE_PATH=${HOME}/dependencies/include - pip install -r requirements-dev.txt -r requirements-freqai.txt + pip install -r requirements-dev.txt pip install -e . - name: Tests diff --git a/build_helpers/install_windows.ps1 b/build_helpers/install_windows.ps1 index 846087b40..4caefa340 100644 --- a/build_helpers/install_windows.ps1 +++ b/build_helpers/install_windows.ps1 @@ -14,5 +14,5 @@ if ($pyv -eq '3.9') { if ($pyv -eq '3.10') { pip install build_helpers\TA_Lib-0.4.24-cp310-cp310-win_amd64.whl } -pip install -r requirements-dev.txt -r requirements-freqai.txt +pip install -r requirements-dev.txt pip install -e . diff --git a/requirements-dev.txt b/requirements-dev.txt index f2f77c2ba..16da829c4 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -2,6 +2,7 @@ -r requirements.txt -r requirements-plot.txt -r requirements-hyperopt.txt +-r requirements-freqai.txt -r docs/requirements-docs.txt coveralls==3.3.1 From c9a6dc88a1774b1ba6d8d02deceadbb68f988b4b Mon Sep 17 00:00:00 2001 From: robcaulk Date: Thu, 21 Jul 2022 11:10:12 +0200 Subject: [PATCH 11/23] add parameter list/discriptions to doc --- docs/freqai.md | 96 ++++++++++++++++++++++++++++++++++++-------------- mkdocs.yml | 2 +- 2 files changed, 70 insertions(+), 28 deletions(-) diff --git a/docs/freqai.md b/docs/freqai.md index a5d7458f5..dc773f3da 100644 --- a/docs/freqai.md +++ b/docs/freqai.md @@ -1,30 +1,26 @@ -# Freqai +# FreqAI -!!! Note - Freqai is still experimental, and should be used at the user's own discretion. - -Freqai is a module designed to automate a variety of tasks associated with +FreqAI is a module designed to automate a variety of tasks associated with training a predictive model to provide signals based on input features. Among the the features included: -* Easy large feature set construction based on simple user input -* Sweep model training and backtesting to simulate consistent model retraining through time -* Smart outlier removal of data points from prediction sets using a Dissimilarity Index. -* Data dimensionality reduction with Principal Component Analysis -* Automatic file management for storage of models to be reused during live -* Smart and safe data standardization -* Cleaning of NaNs from the data set before training and prediction. -* Automated live retraining (still VERY experimental. Proceed with caution.) +* Create large rich feature sets (10k+ features) based on simple user created strategies. +* Sweep model training and backtesting to simulate consistent model retraining through time. +* Remove outliers automatically from training and prediction sets using a Dissimilarity Index and Support Vector Machines. +* Reduce the dimensionality of the data with Principal Component Analysis. +* Store models to disk to make reloading from a crash fast and easy (and purge obsolete files automatically for sustained dry/live runs.) +* Normalize the data automatically in a smart and statistically safe way. +* Automated data download and data handling. +* Clean the incoming data and of NaNs in a safe way and before training and prediction. +* Retrain live automatically so that the model self-adapts to the market in an unsupervised manner. ## General approach The user provides FreqAI with a set of custom indicators (created inside the strategy the same way -a typical Freqtrade strategy is created) as well as a target value (typically some price change into -the future). FreqAI trains a model to predict the target value based on the input of custom indicators. +a typical Freqtrade strategy is created) as well as a target value (typically some price change into the future). FreqAI trains a model to predict the target value based on the input of custom indicators. FreqAI will train and save a new model for each pair in the config whitelist. -Users employ FreqAI to backtest a strategy (emulate reality with retraining a model as new data is -introduced) and run the model live to generate buy and sell signals. +Users employ FreqAI to backtest a strategy (emulate reality with retraining a model as new data is introduced) and run the model live to generate buy and sell signals. In dry/live, FreqAI works in a background thread to keep all models as updated as possible with consistent retraining. ## Background and vocabulary @@ -58,17 +54,55 @@ Use `pip` to install the prerequisites with: ## Running from the example files An example strategy, an example prediction model, and example config can all be found in -`freqtrade/templates/ExampleFreqaiStrategy.py`, -`freqtrade/freqai/prediction_models/CatboostPredictionModel.py`, -`config_examples/config_freqai.example.json`, respectively. Assuming the user has downloaded +`freqtrade/templates/FreqaiExampleStrategy.py`, +`freqtrade/freqai/prediction_models/LightGBMPredictionModel.py`, +`config_examples/config_freqai_futures.example.json`, respectively. Assuming the user has downloaded the necessary data, Freqai can be executed from these templates with: ```bash -freqtrade backtesting --config config_examples/config_freqai.example.json --strategy FreqaiExampleStrategy --freqaimodel CatboostPredictionModel --strategy-path freqtrade/templates --timerange 20220101-20220201 +freqtrade backtesting --config config_examples/config_freqai.example.json --strategy FreqaiExampleStrategy --freqaimodel LightGBMPredictionModel --strategy-path freqtrade/templates --timerange 20220101-20220201 ``` ## Configuring the bot +The table below will list all configuration parameters available for `FreqAI`. + +Mandatory parameters are marked as **Required**, which means that they are required to be set in one of the possible ways. + +| Parameter | Description | +|------------|-------------| +| `freqai` | **Required.** The dictionary containing all the parameters for controlling FreqAI.
**Datatype:** dictionary. +| `identifier` | **Required.** A unique name for the current model. This can be reused to reload pretrained models/data.
**Datatype:** string. +| `train_period_days` | **Required.** Number of days to use for the training data (width of the sliding window).
**Datatype:** positive integer. +| `backtest_period_days` | **Required.** Number of days to inference into the trained model before sliding the window and retraining. This can be fractional days, but beware that the user provided `timerange` will be divided by this number to yield the number of trainings necessary to complete the backtest.
**Datatype:** Float. +| `live_retrain_hours` | Frequency of retraining during dry/live runs. Default set to 0, which means it will retrain as often as possible. **Datatype:** Float > 0. +| `follow_mode` | If true, this instance of FreqAI will look for models associated with `identifier` and load those for inferencing. A `follower` will **not** train new models. False by default.
**Datatype:** boolean. +| `live_trained_timestamp` | Useful if user wants to start from models trained during a *backtest*. The timestamp can be located in the `user_data/models` backtesting folder. This is not a commonly used parameter, leave undefined for most applications.
**Datatype:** positive integer. +| `fit_live_predictions_candles` | Computes target (label) statistics from prediction data, instead of from the training data set. Number of candles is the number of historical candles it uses to generate the statistics.
**Datatype:** positive integer. +| | **Feature Parameters** +| `feature_parameters` | A dictionary containing the parameters used to engineer the feature set. Details and examples shown [here](#building-the-feature-set)
**Datatype:** dictionary. +| `include_corr_pairlist` | A list of correlated coins that FreqAI will add as additional features to all `pair_whitelist` coins. All indicators set in `populate_any_indicators` will be created for each coin in this list, and that set of features is added to the base asset feature set.
**Datatype:** list of assets (strings). +| `include_timeframes` | A list of timeframes that all indicators in `populate_any_indicators` will be created for and added as features to the base asset feature set.
**Datatype:** list of timeframes (strings). +| `label_period_candles` | Number of candles into the future that the labels are created for. This is used in `populate_any_indicators`, refer to `templates/FreqaiExampleStrategy.py` for detailed usage. The user can create custom labels, making use of this parameter not.
**Datatype:** positive integer. +| `include_shifted_candles` | Parameter used to add a sense of temporal recency to flattened regression type input data. `include_shifted_candles` takes all features, duplicates and shifts them by the number indicated by user.
**Datatype:** positive integer. +| `DI_threshold` | Activates the Dissimilarity Index for outlier detection when above 0, explained more [here](#removing-outliers-with-the-dissimilarity-index).
**Datatype:** positive float (typically below 1). +| `weight_factor` | Used to set weights for training data points according to their recency, see details and a figure of how it works [here](##controlling-the-model-learning-process).
**Datatype:** positive float (typically below 1). +| `principal_component_analysis` | Ask FreqAI to automatically reduce the dimensionality of the data set using PCA.
**Datatype:** boolean. +| `use_SVM_to_remove_outliers` | Ask FreqAI to train a support vector machine to detect and remove outliers from the training data set as well as from incoming data points.
**Datatype:** boolean. +| `svm_nu` | The `nu` parameter for the support vector machine. *Very* broadly, this is the percentage of data points that should be considered outliers.
**Datatype:** float between 0 and 1. +| `stratify_training_data` | This value is used to indicate the stratification of the data. e.g. 2 would set every 2nd data point into a separate dataset to be pulled from during training/testing.
**Datatype:** positive integer. +| `indicator_max_period_candles` | The maximum *period* used in `populate_any_indicators()` for indicator creation. FreqAI uses this information in combination with the maximum timeframe to calculate how many data points it should download so that the first data point does not have a NaN
**Datatype:** positive integer. +| `indicator_periods_candles` | A list of integers used to duplicate all indicators according to a set of periods and add them to the feature set.
**Datatype:** list of positive integers. +| | **Data split parameters** +| `data_split_parameters` | include any additional parameters available from Scikit-learn `test_train_split()`, which are shown [here](https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.train_test_split.html)
**Datatype:** dictionary. +| `test_size` | Fraction of data that should be used for testing instead of training.
**Datatype:** positive float below 1. +| `shuffle` | Shuffle the training data points during training. Typically for time-series forecasting, this is set to False. **Datatype:** boolean. +| | **Model training parameters** +| `model_training_parameters` | A flexible dictionary that includes all parameters available by the user selected library. For example, if the user uses `LightGBMPredictionModel`, then this dictionary can contain any parameter available by the `LightGBMRegressor` [here](https://lightgbm.readthedocs.io/en/latest/pythonapi/lightgbm.LGBMRegressor.html). If the user selects a different model, then this dictionary can contain any parameter from that different model.
**Datatype:** dictionary. +| `n_estimators` | A common parameter among regressors which sets the number of boosted trees to fit
**Datatype:** integer. +| `learning_rate` | A common parameter among regressors which sets the boosting learning rate.
**Datatype:** float. +| `n_jobs`, `thread_count`, `task_type` | Different libraries use different parameter names to control the number of threads used for parallel processing or whether or not it is a `task_type` of `gpu` or `cpu`.
**Datatype:** float. + ### Example config file The user interface is isolated to the typical config file. A typical Freqai @@ -115,7 +149,7 @@ components/structures that the user *must* include when building their feature s `with self.model.bridge.lock:` must be used to ensure thread safety - especially when using third party libraries for indicator construction such as TA-lib. Another structure to consider is the location of the labels at the bottom of the example function (below `if set_generalized_indicators:`). -This is where the user will add single features labels to their feature set to avoid duplication from +This is where the user will add single features and labels to their feature set to avoid duplication from various configuration paramters which multiply the feature set such as `include_timeframes`. ```python @@ -213,14 +247,14 @@ a specific pair or timeframe, they should use the following structure inside `po (as exemplified in `freqtrade/templates/FreqaiExampleStrategy.py`: ```python - def populate_any_indicators(self, metadata, pair, df, tf, informative=None, coin=""): + def populate_any_indicators(self, metadata, pair, df, tf, informative=None, coin="", set_generalized_indicators=False): ... # Add generalized indicators here (because in live, it will call only this function to populate # indicators for retraining). Notice how we ensure not to add them multiple times by associating # these generalized indicators to the basepair/timeframe - if pair == metadata['pair'] and tf == self.timeframe: + if set_generalized_indicators: df['%-day_of_week'] = (df["date"].dt.dayofweek + 1) / 7 df['%-hour_of_day'] = (df['date'].dt.hour + 1) / 25 @@ -292,7 +326,7 @@ and adding this to the `train_period_days`. The units need to be in the base can The freqai training/backtesting module can be executed with the following command: ```bash -freqtrade backtesting --strategy FreqaiExampleStrategy --config config_freqai.example.json --freqaimodel CatboostPredictionModel --timerange 20210501-20210701 +freqtrade backtesting --strategy FreqaiExampleStrategy --config config_freqai_futures.example.json --freqaimodel LightGBMPredictionModel --timerange 20210501-20210701 ``` If this command has never been executed with the existing config file, then it will train a new model @@ -370,7 +404,7 @@ the feature set with a proper naming convention for the IFreqaiModel to use late ### Building an IFreqaiModel -Freqai has an example prediction model based on the popular `Catboost` regression (`freqai/prediction_models/CatboostPredictionModel.py`). However, users can customize and create +FreqAI has multiple example prediction model based libraries such as `Catboost` regression (`freqai/prediction_models/CatboostPredictionModel.py`) and `LightGBM` regression. However, users can customize and create their own prediction models using the `IFreqaiModel` class. Users are encouraged to inherit `train()` and `predict()` to let them customize various aspects of their training procedures. ### Running the model live @@ -443,7 +477,7 @@ $\overline{d}$ quantifies the spread of the training data, which is compared to the distance between the new prediction feature vectors, $X_k$ and all the training data: -$$ d_k = \argmin_i d_{k,i} $$ +$$ d_k = \arg \min d_{k,i} $$ which enables the estimation of a Dissimilarity Index: @@ -635,6 +669,14 @@ below this value. An example usage in the strategy may look something like: ## Additional information +### Common pitfalls + +FreqAI cannot be combined with `VolumePairlists` (or any pairlist filter that adds and removes pairs dynamically). +This is for performance reasons - FreqAI relies on making quick predictions/retrains. To do this effectively, +it needs to download all the training data at the beginning of a dry/live instance. FreqAI stores and appends +new candles automatically for future retrains. But this means that if new pairs arrive later in the dry run due +to a volume pairlist, it will not have the data ready. FreqAI does work, however, with the `ShufflePairlist`. + ### Feature normalization The feature set created by the user is automatically normalized to the training diff --git a/mkdocs.yml b/mkdocs.yml index 18744e0d5..b084b59a7 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -35,7 +35,7 @@ nav: - Edge Positioning: edge.md - Advanced Strategy: strategy-advanced.md - Advanced Hyperopt: advanced-hyperopt.md - - Freqai: freqai.md + - FreqAI: freqai.md - Sandbox Testing: sandbox-testing.md - FAQ: faq.md - SQL Cheat-sheet: sql_cheatsheet.md From e7337728bf4fcc9ee1a885baa15c13f7733dd10e Mon Sep 17 00:00:00 2001 From: robcaulk Date: Thu, 21 Jul 2022 11:25:28 +0200 Subject: [PATCH 12/23] add separator in folder name just incase an asset ends in an integer --- freqtrade/freqai/data_drawer.py | 2 +- freqtrade/freqai/data_kitchen.py | 47 ++++++++++++++++------------ freqtrade/freqai/freqai_interface.py | 1 + 3 files changed, 29 insertions(+), 21 deletions(-) diff --git a/freqtrade/freqai/data_drawer.py b/freqtrade/freqai/data_drawer.py index 5488a7e6b..c89394c09 100644 --- a/freqtrade/freqai/data_drawer.py +++ b/freqtrade/freqai/data_drawer.py @@ -301,7 +301,7 @@ class FreqaiDataDrawer: model_folders = [x for x in self.full_path.iterdir() if x.is_dir()] - pattern = re.compile(r"sub-train-(\w+)(\d{10})") + pattern = re.compile(r"sub-train-(\w+)_(\d{10})") delete_dict: Dict[str, Any] = {} diff --git a/freqtrade/freqai/data_kitchen.py b/freqtrade/freqai/data_kitchen.py index 0966f8421..808a8a8ba 100644 --- a/freqtrade/freqai/data_kitchen.py +++ b/freqtrade/freqai/data_kitchen.py @@ -88,7 +88,8 @@ class FreqaiDataKitchen: ) self.data_path = Path( - self.full_path / str("sub-train" + "-" + pair.split("/")[0] + str(trained_timestamp)) + self.full_path + / str("sub-train" + "-" + pair.split("/")[0] + "_" + str(trained_timestamp)) ) return @@ -179,6 +180,7 @@ class FreqaiDataKitchen: model = load(self.data_path / str(self.model_filename + "_model.joblib")) else: from tensorflow import keras + model = keras.models.load_model(self.data_path / str(self.model_filename + "_model.h5")) if Path(self.data_path / str(self.model_filename + "_svm_model.joblib")).resolve().exists(): @@ -412,8 +414,7 @@ class FreqaiDataKitchen: if not isinstance(train_split, int) or train_split < 1: raise OperationalException( - "train_period_days must be an integer greater than 0. " - f"Got {train_split}." + "train_period_days must be an integer greater than 0. " f"Got {train_split}." ) train_period_days = train_split * SECONDS_IN_DAY bt_period = bt_split * SECONDS_IN_DAY @@ -566,8 +567,10 @@ class FreqaiDataKitchen: """ if self.keras: - logger.warning("SVM outlier removal not currently supported for Keras based models. " - "Skipping user requested function.") + logger.warning( + "SVM outlier removal not currently supported for Keras based models. " + "Skipping user requested function." + ) if predict: self.do_predict = np.ones(len(self.data_dictionary["prediction_features"])) return @@ -681,8 +684,7 @@ class FreqaiDataKitchen: training than older data. """ wfactor = self.config["freqai"]["feature_parameters"]["weight_factor"] - weights = np.exp( - - np.arange(num_weights) / (wfactor * num_weights))[::-1] + weights = np.exp(-np.arange(num_weights) / (wfactor * num_weights))[::-1] return weights def append_predictions(self, predictions, do_predict, len_dataframe): @@ -731,10 +733,10 @@ class FreqaiDataKitchen: def create_fulltimerange(self, backtest_tr: str, backtest_period_days: int) -> str: if not isinstance(backtest_period_days, int): - raise OperationalException('backtest_period_days must be an integer') + raise OperationalException("backtest_period_days must be an integer") if backtest_period_days < 0: - raise OperationalException('backtest_period_days must be positive') + raise OperationalException("backtest_period_days must be positive") backtest_timerange = TimeRange.parse_timerange(backtest_tr) @@ -743,8 +745,9 @@ class FreqaiDataKitchen: datetime.datetime.now(tz=datetime.timezone.utc).timestamp() ) - backtest_timerange.startts = (backtest_timerange.startts - - backtest_period_days * SECONDS_IN_DAY) + backtest_timerange.startts = ( + backtest_timerange.startts - backtest_period_days * SECONDS_IN_DAY + ) start = datetime.datetime.utcfromtimestamp(backtest_timerange.startts) stop = datetime.datetime.utcfromtimestamp(backtest_timerange.stopts) full_timerange = start.strftime("%Y%m%d") + "-" + stop.strftime("%Y%m%d") @@ -790,8 +793,9 @@ class FreqaiDataKitchen: data_load_timerange = TimeRange() # find the max indicator length required - max_timeframe_chars = self.freqai_config.get( - "feature_parameters", {}).get("include_timeframes")[-1] + max_timeframe_chars = self.freqai_config.get("feature_parameters", {}).get( + "include_timeframes" + )[-1] max_period = self.freqai_config.get("feature_parameters", {}).get( "indicator_max_period_candles", 50 ) @@ -858,7 +862,7 @@ class FreqaiDataKitchen: coin, _ = pair.split("/") self.data_path = Path( self.full_path - / str("sub-train" + "-" + pair.split("/")[0] + str(int(trained_timerange.stopts))) + / str("sub-train" + "-" + pair.split("/")[0] + "_" + str(int(trained_timerange.stopts))) ) self.model_filename = "cb_" + coin.lower() + "_" + str(int(trained_timerange.stopts)) @@ -942,8 +946,9 @@ class FreqaiDataKitchen: def set_all_pairs(self) -> None: - self.all_pairs = copy.deepcopy(self.freqai_config.get( - 'feature_parameters', {}).get('include_corr_pairlist', [])) + self.all_pairs = copy.deepcopy( + self.freqai_config.get("feature_parameters", {}).get("include_corr_pairlist", []) + ) for pair in self.config.get("exchange", "").get("pair_whitelist"): if pair not in self.all_pairs: self.all_pairs.append(pair) @@ -987,8 +992,9 @@ class FreqaiDataKitchen: corr_dataframes: Dict[Any, Any] = {} base_dataframes: Dict[Any, Any] = {} historic_data = self.dd.historic_data - pairs = self.freqai_config.get('feature_parameters', {}).get( - 'include_corr_pairlist', []) + pairs = self.freqai_config.get("feature_parameters", {}).get( + "include_corr_pairlist", [] + ) for tf in self.freqai_config.get("feature_parameters", {}).get("include_timeframes"): base_dataframes[tf] = self.slice_dataframe(timerange, historic_data[pair][tf]) @@ -1053,7 +1059,7 @@ class FreqaiDataKitchen: dataframe: DataFrame = dataframe containing populated indicators """ dataframe = base_dataframes[self.config["timeframe"]].copy() - pairs = self.freqai_config.get('feature_parameters', {}).get('include_corr_pairlist', []) + pairs = self.freqai_config.get("feature_parameters", {}).get("include_corr_pairlist", []) sgi = True for tf in self.freqai_config.get("feature_parameters", {}).get("include_timeframes"): dataframe = strategy.populate_any_indicators( @@ -1086,7 +1092,8 @@ class FreqaiDataKitchen: Fit the labels with a gaussian distribution """ import scipy as spy - num_candles = self.freqai_config.get('fit_live_predictions_candles', 100) + + num_candles = self.freqai_config.get("fit_live_predictions_candles", 100) self.data["labels_mean"], self.data["labels_std"] = {}, {} for label in self.label_list: f = spy.stats.norm.fit(self.dd.historic_predictions[self.pair][label].tail(num_candles)) diff --git a/freqtrade/freqai/freqai_interface.py b/freqtrade/freqai/freqai_interface.py index 7ca43c0fe..7631a4d25 100644 --- a/freqtrade/freqai/freqai_interface.py +++ b/freqtrade/freqai/freqai_interface.py @@ -189,6 +189,7 @@ class IFreqaiModel(ABC): "sub-train" + "-" + metadata["pair"].split("/")[0] + + "_" + str(int(trained_timestamp.stopts)) ) ) From 8f86b0deaa08f53b28da9e05b6fe6779b79b8001 Mon Sep 17 00:00:00 2001 From: robcaulk Date: Thu, 21 Jul 2022 12:24:22 +0200 Subject: [PATCH 13/23] *breaking change* simplify user strat by consolidating feature loops into backend --- docs/freqai.md | 29 ++++--------------- freqtrade/freqai/data_kitchen.py | 30 ++++++++++++++++---- freqtrade/freqai/freqai_interface.py | 4 +++ freqtrade/templates/FreqaiExampleStrategy.py | 29 ++++--------------- tests/strategy/strats/freqai_test_strat.py | 22 +------------- 5 files changed, 42 insertions(+), 72 deletions(-) diff --git a/docs/freqai.md b/docs/freqai.md index dc773f3da..aa53cac6b 100644 --- a/docs/freqai.md +++ b/docs/freqai.md @@ -368,32 +368,15 @@ The Freqai strategy requires the user to include the following lines of code in def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: self.freqai_info = self.config["freqai"] - self.pair = metadata["pair"] - sgi = True - # the following loops are necessary for building the features - # indicated by the user in the configuration file. + # All indicators must be populated by populate_any_indicators() for live functionality # to work correctly. - for tf in self.freqai_info["feature_parameters"]["include_timeframes"]: - dataframe = self.populate_any_indicators( - metadata, - self.pair, - dataframe.copy(), - tf, - coin=self.pair.split("/")[0] + "-", - set_generalized_indicators=sgi, - ) - sgi = False - for pair in self.freqai_info["feature_parameters"]["include_corr_pairlist"]: - if metadata["pair"] in pair: - continue # do not include whitelisted pair twice if it is in corr_pairlist - dataframe = self.populate_any_indicators( - metadata, pair, dataframe.copy(), tf, coin=pair.split("/")[0] + "-" - ) - # the model will return 4 values, its prediction, an indication of whether or not the - # prediction should be accepted, the target mean/std values from the labels used during - # each training period. + # the model will return all labels created by user in `populate_any_indicators` + # (& appended targets), an indication of whether or not the prediction should be accepted, + # the target mean/std values for each of the labels created by user in + # `populate_any_indicators()` for each training period. + dataframe = self.model.bridge.start(dataframe, metadata, self) return dataframe diff --git a/freqtrade/freqai/data_kitchen.py b/freqtrade/freqai/data_kitchen.py index 808a8a8ba..11105fd67 100644 --- a/freqtrade/freqai/data_kitchen.py +++ b/freqtrade/freqai/data_kitchen.py @@ -1043,7 +1043,12 @@ class FreqaiDataKitchen: # return corr_dataframes, base_dataframes def use_strategy_to_populate_indicators( - self, strategy: IStrategy, corr_dataframes: dict, base_dataframes: dict, pair: str + self, + strategy: IStrategy, + corr_dataframes: dict = {}, + base_dataframes: dict = {}, + pair: str = "", + prediction_dataframe: DataFrame = pd.DataFrame(), ) -> DataFrame: """ Use the user defined strategy for populating indicators during @@ -1058,16 +1063,31 @@ class FreqaiDataKitchen: :returns: dataframe: DataFrame = dataframe containing populated indicators """ - dataframe = base_dataframes[self.config["timeframe"]].copy() + + # for prediction dataframe creation, we let dataprovider handle everything in the strategy + # so we create empty dictionaries, which allows us to pass None to + # `populate_any_indicators()`. Signaling we want the dp to give us the live dataframe. + tfs = self.freqai_config.get("feature_parameters", {}).get("include_timeframes") pairs = self.freqai_config.get("feature_parameters", {}).get("include_corr_pairlist", []) + if not prediction_dataframe.empty: + dataframe = prediction_dataframe.copy() + for tf in tfs: + base_dataframes[tf] = None + for p in pairs: + if p not in corr_dataframes: + corr_dataframes[p] = {} + corr_dataframes[p][tf] = None + else: + dataframe = base_dataframes[self.config["timeframe"]].copy() + sgi = True - for tf in self.freqai_config.get("feature_parameters", {}).get("include_timeframes"): + for tf in tfs: dataframe = strategy.populate_any_indicators( pair, pair, dataframe.copy(), tf, - base_dataframes[tf], + informative=base_dataframes[tf], coin=pair.split("/")[0] + "-", set_generalized_indicators=sgi, ) @@ -1081,7 +1101,7 @@ class FreqaiDataKitchen: i, dataframe.copy(), tf, - corr_dataframes[i][tf], + informative=corr_dataframes[i][tf], coin=i.split("/")[0] + "-", ) diff --git a/freqtrade/freqai/freqai_interface.py b/freqtrade/freqai/freqai_interface.py index 7631a4d25..06892b90a 100644 --- a/freqtrade/freqai/freqai_interface.py +++ b/freqtrade/freqai/freqai_interface.py @@ -281,6 +281,10 @@ class IFreqaiModel(ABC): # load the model and associated data into the data kitchen self.model = dk.load_data(coin=metadata["pair"]) + dataframe = self.dk.use_strategy_to_populate_indicators( + strategy, prediction_dataframe=dataframe, pair=metadata["pair"] + ) + if not self.model: logger.warning( f"No model ready for {metadata['pair']}, returning null values to strategy." diff --git a/freqtrade/templates/FreqaiExampleStrategy.py b/freqtrade/templates/FreqaiExampleStrategy.py index ca08b8168..402aa9d1c 100644 --- a/freqtrade/templates/FreqaiExampleStrategy.py +++ b/freqtrade/templates/FreqaiExampleStrategy.py @@ -171,32 +171,15 @@ class FreqaiExampleStrategy(IStrategy): def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: self.freqai_info = self.config["freqai"] - self.pair = metadata["pair"] - sgi = True - # the following loops are necessary for building the features - # indicated by the user in the configuration file. + # All indicators must be populated by populate_any_indicators() for live functionality # to work correctly. - for tf in self.freqai_info["feature_parameters"]["include_timeframes"]: - dataframe = self.populate_any_indicators( - metadata, - self.pair, - dataframe.copy(), - tf, - coin=self.pair.split("/")[0] + "-", - set_generalized_indicators=sgi, - ) - sgi = False - for pair in self.freqai_info["feature_parameters"]["include_corr_pairlist"]: - if metadata["pair"] in pair: - continue # do not include whitelisted pair twice if it is in corr_pairlist - dataframe = self.populate_any_indicators( - metadata, pair, dataframe.copy(), tf, coin=pair.split("/")[0] + "-" - ) - # the model will return 4 values, its prediction, an indication of whether or not the - # prediction should be accepted, the target mean/std values from the labels used during - # each training period. + # the model will return all labels created by user in `populate_any_indicators` + # (& appended targets), an indication of whether or not the prediction should be accepted, + # the target mean/std values for each of the labels created by user in + # `populate_any_indicators()` for each training period. + dataframe = self.model.bridge.start(dataframe, metadata, self) dataframe["target_roi"] = dataframe["&-s_close_mean"] + dataframe["&-s_close_std"] * 1.25 diff --git a/tests/strategy/strats/freqai_test_strat.py b/tests/strategy/strats/freqai_test_strat.py index e2e823adb..28e3dce54 100644 --- a/tests/strategy/strats/freqai_test_strat.py +++ b/tests/strategy/strats/freqai_test_strat.py @@ -140,29 +140,9 @@ class freqai_test_strat(IStrategy): def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: self.freqai_info = self.config["freqai"] - self.pair = metadata["pair"] - sgi = True - # the following loops are necessary for building the features - # indicated by the user in the configuration file. + # All indicators must be populated by populate_any_indicators() for live functionality # to work correctly. - for tf in self.freqai_info["feature_parameters"]["include_timeframes"]: - dataframe = self.populate_any_indicators( - metadata, - self.pair, - dataframe.copy(), - tf, - coin=self.pair.split("/")[0] + "-", - set_generalized_indicators=sgi, - ) - sgi = False - for pair in self.freqai_info["feature_parameters"]["include_corr_pairlist"]: - if metadata["pair"] in pair: - continue # do not include whitelisted pair twice if it is in corr_pairlist - dataframe = self.populate_any_indicators( - metadata, pair, dataframe.copy(), tf, coin=pair.split("/")[0] + "-" - ) - # the model will return 4 values, its prediction, an indication of whether or not the # prediction should be accepted, the target mean/std values from the labels used during # each training period. From ca4dd58642a6166a11bff8fd024057729b7cacbe Mon Sep 17 00:00:00 2001 From: robcaulk Date: Thu, 21 Jul 2022 12:40:54 +0200 Subject: [PATCH 14/23] remove superceded function from datakitchen --- freqtrade/freqai/data_kitchen.py | 32 -------------------------------- 1 file changed, 32 deletions(-) diff --git a/freqtrade/freqai/data_kitchen.py b/freqtrade/freqai/data_kitchen.py index 11105fd67..162ee9380 100644 --- a/freqtrade/freqai/data_kitchen.py +++ b/freqtrade/freqai/data_kitchen.py @@ -1010,38 +1010,6 @@ class FreqaiDataKitchen: return corr_dataframes, base_dataframes - # SUPERCEDED - # def load_pairs_histories(self, timerange: TimeRange, metadata: dict) -> Tuple[Dict[Any, Any], - # DataFrame]: - # corr_dataframes: Dict[Any, Any] = {} - # base_dataframes: Dict[Any, Any] = {} - # pairs = self.freqai_config.get('include_corr_pairlist', []) # + [metadata['pair']] - # # timerange = TimeRange.parse_timerange(new_timerange) - - # for tf in self.freqai_config.get('timeframes'): - # base_dataframes[tf] = load_pair_history(datadir=self.config['datadir'], - # timeframe=tf, - # pair=metadata['pair'], timerange=timerange, - # data_format=self.config.get( - # 'dataformat_ohlcv', 'json'), - # candle_type=self.config.get( - # 'trading_mode', 'spot')) - # if pairs: - # for p in pairs: - # if metadata['pair'] in p: - # continue # dont repeat anything from whitelist - # if p not in corr_dataframes: - # corr_dataframes[p] = {} - # corr_dataframes[p][tf] = load_pair_history(datadir=self.config['datadir'], - # timeframe=tf, - # pair=p, timerange=timerange, - # data_format=self.config.get( - # 'dataformat_ohlcv', 'json'), - # candle_type=self.config.get( - # 'trading_mode', 'spot')) - - # return corr_dataframes, base_dataframes - def use_strategy_to_populate_indicators( self, strategy: IStrategy, From e694ea1cfda6fc5e372313853b1eed658599f3a6 Mon Sep 17 00:00:00 2001 From: robcaulk Date: Thu, 21 Jul 2022 12:48:09 +0200 Subject: [PATCH 15/23] make sure backtesting gets the populated indicators with slimmed down user strat --- freqtrade/freqai/freqai_interface.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/freqtrade/freqai/freqai_interface.py b/freqtrade/freqai/freqai_interface.py index 06892b90a..c940ff0d3 100644 --- a/freqtrade/freqai/freqai_interface.py +++ b/freqtrade/freqai/freqai_interface.py @@ -106,6 +106,10 @@ class IFreqaiModel(ABC): elif not self.follow_mode: self.dk = FreqaiDataKitchen(self.config, self.dd, self.live, metadata["pair"]) logger.info(f"Training {len(self.dk.training_timeranges)} timeranges") + + dataframe = self.dk.use_strategy_to_populate_indicators( + strategy, prediction_dataframe=dataframe, pair=metadata["pair"] + ) dk = self.start_backtesting(dataframe, metadata, self.dk) dataframe = self.remove_features_from_df(dk.return_dataframe) From 183dec866a47186b0f52c6c3d7257ba7fda1fce8 Mon Sep 17 00:00:00 2001 From: robcaulk Date: Thu, 21 Jul 2022 13:02:52 +0200 Subject: [PATCH 16/23] remove ability to backtest open ended timeranges (safer) --- freqtrade/freqai/data_kitchen.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/freqtrade/freqai/data_kitchen.py b/freqtrade/freqai/data_kitchen.py index 162ee9380..132faaa86 100644 --- a/freqtrade/freqai/data_kitchen.py +++ b/freqtrade/freqai/data_kitchen.py @@ -741,9 +741,16 @@ class FreqaiDataKitchen: backtest_timerange = TimeRange.parse_timerange(backtest_tr) if backtest_timerange.stopts == 0: - backtest_timerange.stopts = int( - datetime.datetime.now(tz=datetime.timezone.utc).timestamp() - ) + # typically open ended time ranges do work, however, there are some edge cases where + # it does not. accomodating these kinds of edge cases just to allow open-ended + # timerange is not high enough priority to warrant the effort. It is safer for now + # to simply ask user to add their end date + raise OperationalException("FreqAI backtesting does not allow open ended timeranges. " + "Please indicate the end date of your desired backtesting. " + "timerange.") + # backtest_timerange.stopts = int( + # datetime.datetime.now(tz=datetime.timezone.utc).timestamp() + # ) backtest_timerange.startts = ( backtest_timerange.startts - backtest_period_days * SECONDS_IN_DAY From 8033e0bf2334456dab605f7766748d9130c1ae7b Mon Sep 17 00:00:00 2001 From: robcaulk Date: Thu, 21 Jul 2022 13:22:12 +0200 Subject: [PATCH 17/23] add counter to backtesting log so users know how many more pairs and how many more models will need to be trained --- freqtrade/freqai/freqai_interface.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqai/freqai_interface.py b/freqtrade/freqai/freqai_interface.py index c940ff0d3..1d152b702 100644 --- a/freqtrade/freqai/freqai_interface.py +++ b/freqtrade/freqai/freqai_interface.py @@ -73,6 +73,8 @@ class IFreqaiModel(ABC): self.freqai_info["feature_parameters"]["DI_threshold"] = 0 logger.warning("DI threshold is not configured for Keras models yet. Deactivating.") self.CONV_WIDTH = self.freqai_info.get("conv_width", 2) + self.pair_it = 0 + self.total_pairs = len(self.config.get("exchange", {}).get("pair_whitelist")) def assert_config(self, config: Dict[str, Any]) -> None: @@ -164,6 +166,8 @@ class IFreqaiModel(ABC): dk: FreqaiDataKitchen = Data management/analysis tool assoicated to present pair only """ + self.pair_it += 1 + train_it = 0 # Loop enforcing the sliding window training/backtesting paradigm # tr_train is the training time range e.g. 1 historical month # tr_backtest is the backtesting time range e.g. the week directly @@ -171,6 +175,8 @@ class IFreqaiModel(ABC): # entire backtest for tr_train, tr_backtest in zip(dk.training_timeranges, dk.backtesting_timeranges): (_, _, _, _) = self.dd.get_pair_dict_info(metadata["pair"]) + train_it += 1 + total_trains = len(dk.backtesting_timeranges) gc.collect() dk.data = {} # clean the pair specific data between training window sliding self.training_timerange = tr_train @@ -184,8 +190,11 @@ class IFreqaiModel(ABC): tr_train_stopts_str = datetime.datetime.utcfromtimestamp(tr_train.stopts).strftime( "%Y-%m-%d %H:%M:%S" ) - logger.info(f"Training {metadata['pair']}") - logger.info(f" from {tr_train_startts_str} to {tr_train_stopts_str}") + logger.info( + f"Training {metadata['pair']}, {self.pair_it}/{self.total_pairs} pairs" + f" from {tr_train_startts_str} to {tr_train_stopts_str}, {train_it}/{total_trains} " + "trains" + ) dk.data_path = Path( dk.full_path From 3205788bce2b0ddf9405eddda7888af0d1acc372 Mon Sep 17 00:00:00 2001 From: robcaulk Date: Thu, 21 Jul 2022 22:11:46 +0200 Subject: [PATCH 18/23] extend doc to include descriptions of the return values from FreqAI to the strategy --- docs/freqai.md | 14 +++++++++++++- freqtrade/freqai/data_kitchen.py | 2 +- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/docs/freqai.md b/docs/freqai.md index aa53cac6b..1d1fd8cfe 100644 --- a/docs/freqai.md +++ b/docs/freqai.md @@ -79,6 +79,9 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `follow_mode` | If true, this instance of FreqAI will look for models associated with `identifier` and load those for inferencing. A `follower` will **not** train new models. False by default.
**Datatype:** boolean. | `live_trained_timestamp` | Useful if user wants to start from models trained during a *backtest*. The timestamp can be located in the `user_data/models` backtesting folder. This is not a commonly used parameter, leave undefined for most applications.
**Datatype:** positive integer. | `fit_live_predictions_candles` | Computes target (label) statistics from prediction data, instead of from the training data set. Number of candles is the number of historical candles it uses to generate the statistics.
**Datatype:** positive integer. +| `purge_old_models` | Tell FreqAI to delete obsolete models. Otherwise, all historic models will remain on disk. Defaults to False.
**Datatype:** boolean. +| | **Feature Parameters** +| `expiration_hours` | Ask FreqAI to avoid making predictions if a model is more than `expiration_hours` old. Defaults to 0 which means models never expire.
**Datatype:** positive integer. | | **Feature Parameters** | `feature_parameters` | A dictionary containing the parameters used to engineer the feature set. Details and examples shown [here](#building-the-feature-set)
**Datatype:** dictionary. | `include_corr_pairlist` | A list of correlated coins that FreqAI will add as additional features to all `pair_whitelist` coins. All indicators set in `populate_any_indicators` will be created for each coin in this list, and that set of features is added to the base asset feature set.
**Datatype:** list of assets (strings). @@ -103,6 +106,15 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `learning_rate` | A common parameter among regressors which sets the boosting learning rate.
**Datatype:** float. | `n_jobs`, `thread_count`, `task_type` | Different libraries use different parameter names to control the number of threads used for parallel processing or whether or not it is a `task_type` of `gpu` or `cpu`.
**Datatype:** float. +Here are the values you can expect to receive inside the dataframe returned by FreqAI: + +| Parameter | Description | +|------------|-------------| +| `&-s*` | user defined labels in the user made strategy. Anything prepended with `&` is treated as a training target inside FreqAI. These same dataframe columns names are fed back to the user as the predictions. For example, the user wishes to predict the price change in the next 40 candles (similar to `templates/FreqaiExampleStrategy.py`) by setting `&-s_close`. FreqAI makes the predictions and gives them back to the user under the same key (`&-s_close`) to be used in `populate_entry/exit_trend()`.
**Datatype:** depends on the output of the model. +| `&-s*_std/mean` | The standard deviation and mean values of the user defined labels during training (or live tracking with `fit_live_predictions_candles`). Commonly used to understand rarity of prediction (use the z-score as shown in `templates/FreqaiExampleStrategy.py` to evaluate how often a particular prediction was observed during training (or historically with `fit_live_predictions_candles`)
**Datatype:** floats. +| `do_predict` | An indication of an outlier, this return value is integer between -1 and 2 which lets the user understand if the prediction is trustworthy or not. `do_predict==1` means the prediction is trustworthy. If the [Dissimilartiy Index](#removing-outliers-with-the-dissimilarity-index) is above the user defined treshold, it will subtract 1 from `do_predict`. If `use_SVM_to_remove_outliers()` is active, then the Support Vector Machine (SVM) may also detect outliers in training and prediction data. In this case, the SVM will also subtract one from `do_predict`. A particular case is when `do_predict == 2`, it means that the model has expired due to `expired_hours`.
**Datatype:** integer between -1 and 2. +| `DI_values` | The raw Dissimilarity Index values to give the user a sense of confidence in the prediction. Lower DI means the data point is closer to the trained parameter space.
**Datatype:** float. + ### Example config file The user interface is isolated to the typical config file. A typical Freqai @@ -376,7 +388,7 @@ The Freqai strategy requires the user to include the following lines of code in # (& appended targets), an indication of whether or not the prediction should be accepted, # the target mean/std values for each of the labels created by user in # `populate_any_indicators()` for each training period. - + dataframe = self.model.bridge.start(dataframe, metadata, self) return dataframe diff --git a/freqtrade/freqai/data_kitchen.py b/freqtrade/freqai/data_kitchen.py index 132faaa86..1100d04f0 100644 --- a/freqtrade/freqai/data_kitchen.py +++ b/freqtrade/freqai/data_kitchen.py @@ -69,7 +69,7 @@ class FreqaiDataKitchen: config["freqai"]["train_period_days"], config["freqai"]["backtest_period_days"], ) - # self.strat_dataframe: DataFrame = strat_dataframe + self.dd = data_drawer def set_paths( From ac0f484918722d8f97e274d4a1e5c6003b44b329 Mon Sep 17 00:00:00 2001 From: robcaulk Date: Fri, 22 Jul 2022 00:02:07 +0200 Subject: [PATCH 19/23] add freqai logo to top of doc --- docs/assets/freqai_logo_no_md.svg | 221 ++++++++++++++++++++++++++++++ docs/freqai.md | 4 +- 2 files changed, 224 insertions(+), 1 deletion(-) create mode 100644 docs/assets/freqai_logo_no_md.svg diff --git a/docs/assets/freqai_logo_no_md.svg b/docs/assets/freqai_logo_no_md.svg new file mode 100644 index 000000000..2d1305462 --- /dev/null +++ b/docs/assets/freqai_logo_no_md.svg @@ -0,0 +1,221 @@ + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + FreqAI + + diff --git a/docs/freqai.md b/docs/freqai.md index 1d1fd8cfe..488b82e91 100644 --- a/docs/freqai.md +++ b/docs/freqai.md @@ -1,3 +1,5 @@ +![freqai-logo](assets/freqai_logo_no_md.svg) + # FreqAI FreqAI is a module designed to automate a variety of tasks associated with @@ -516,7 +518,7 @@ The user can tell Freqai to remove outlier data points from the training/test da ```json "freqai": { "feature_parameters" : { - "use_SVM_to_remove_outliers: true + "use_SVM_to_remove_outliers": true } } ``` From 0b21750e768376ef903b51069d0f94a121c0b33c Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 22 Jul 2022 07:22:01 +0200 Subject: [PATCH 20/23] Reorder advanced topics --- mkdocs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mkdocs.yml b/mkdocs.yml index b084b59a7..ebbd65a0f 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -32,10 +32,10 @@ nav: - Backtest analysis: advanced-backtesting.md - Advanced Topics: - Advanced Post-installation Tasks: advanced-setup.md - - Edge Positioning: edge.md - Advanced Strategy: strategy-advanced.md - Advanced Hyperopt: advanced-hyperopt.md - FreqAI: freqai.md + - Edge Positioning: edge.md - Sandbox Testing: sandbox-testing.md - FAQ: faq.md - SQL Cheat-sheet: sql_cheatsheet.md From afcb0bec00aa708d6b4ac7d4445d437dcf3094bd Mon Sep 17 00:00:00 2001 From: robcaulk Date: Fri, 22 Jul 2022 12:17:15 +0200 Subject: [PATCH 21/23] clean up obsolete comments, move remove_features_from_df to datakitchen --- freqtrade/freqai/data_kitchen.py | 10 +++++ freqtrade/freqai/freqai_interface.py | 55 ++++------------------------ 2 files changed, 18 insertions(+), 47 deletions(-) diff --git a/freqtrade/freqai/data_kitchen.py b/freqtrade/freqai/data_kitchen.py index 1100d04f0..cfa0d3818 100644 --- a/freqtrade/freqai/data_kitchen.py +++ b/freqtrade/freqai/data_kitchen.py @@ -1116,6 +1116,16 @@ class FreqaiDataKitchen: # self.data["lower_quantile"] = lower_q return + def remove_features_from_df(self, dataframe: DataFrame) -> DataFrame: + """ + Remove the features from the dataframe before returning it to strategy. This keeps it + compact for Frequi purposes. + """ + to_keep = [ + col for col in dataframe.columns if not col.startswith("%") or col.startswith("%%") + ] + return dataframe[to_keep] + def np_encoder(self, object): if isinstance(object, np.generic): return object.item() diff --git a/freqtrade/freqai/freqai_interface.py b/freqtrade/freqai/freqai_interface.py index 1d152b702..7f2fd677c 100644 --- a/freqtrade/freqai/freqai_interface.py +++ b/freqtrade/freqai/freqai_interface.py @@ -37,9 +37,7 @@ def threaded(fn): class IFreqaiModel(ABC): """ Class containing all tools for training and prediction in the strategy. - User models should inherit from this class as shown in - templates/ExamplePredictionModel.py where the user overrides - train(), predict(), fit(), and make_labels(). + Base*PredictionModels inherit from this class. Author: Robert Caulk, rob.caulk@gmail.com """ @@ -51,23 +49,15 @@ class IFreqaiModel(ABC): self.data_split_parameters = config.get("freqai", {}).get("data_split_parameters") self.model_training_parameters = config.get("freqai", {}).get("model_training_parameters") self.feature_parameters = config.get("freqai", {}).get("feature_parameters") - self.time_last_trained = None - self.current_time = None self.model = None - self.predictions = None - self.training_on_separate_thread = False self.retrain = False self.first = True - self.update_historic_data = 0 self.set_full_path() self.follow_mode = self.freqai_info.get("follow_mode", False) self.dd = FreqaiDataDrawer(Path(self.full_path), self.config, self.follow_mode) self.lock = threading.Lock() - self.follow_mode = self.freqai_info.get("follow_mode", False) self.identifier = self.freqai_info.get("identifier", "no_id_provided") self.scanning = False - self.ready_to_scan = False - self.first = True self.keras = self.freqai_info.get("keras", False) if self.keras and self.freqai_info.get("feature_parameters", {}).get("DI_threshold", 0): self.freqai_info["feature_parameters"]["DI_threshold"] = 0 @@ -114,7 +104,7 @@ class IFreqaiModel(ABC): ) dk = self.start_backtesting(dataframe, metadata, self.dk) - dataframe = self.remove_features_from_df(dk.return_dataframe) + dataframe = dk.remove_features_from_df(dk.return_dataframe) return self.return_values(dataframe, dk) @threaded @@ -260,9 +250,6 @@ class IFreqaiModel(ABC): dk.update_historic_data(strategy) logger.debug(f'Updating historic data on pair {metadata["pair"]}') - # if trainable, check if model needs training, if so compute new timerange, - # then save model and metadata. - # if not trainable, load existing data if not self.follow_mode: (_, new_trained_timerange, data_load_timerange) = dk.check_if_new_training_required( @@ -320,6 +307,8 @@ class IFreqaiModel(ABC): # correct array to strategy if pair not in self.dd.model_return_values: + # first predictions are made on entire historical candle set coming from strategy. This + # allows FreqUI to show full return values. pred_df, do_preds = self.predict(dataframe, dk) self.dd.set_initial_return_values(pair, dk, pred_df, do_preds) dk.return_dataframe = self.dd.attach_return_values_to_return_dataframe(pair, dataframe) @@ -333,7 +322,8 @@ class IFreqaiModel(ABC): "prediction == 0 and do_predict == 2" ) else: - # Only feed in the most recent candle for prediction in live scenario + # remaining predictions are made only on the most recent candles for performance and + # historical accuracy reasons. pred_df, do_preds = self.predict(dataframe.iloc[-self.CONV_WIDTH:], dk, first=False) self.dd.append_model_predictions(pair, pred_df, do_preds, dk, len(dataframe)) @@ -384,11 +374,6 @@ class IFreqaiModel(ABC): if self.freqai_info.get("feature_parameters", {}).get("DI_threshold", 0): dk.data["avg_mean_dist"] = dk.compute_distances() - # if self.feature_parameters["determine_statistical_distributions"]: - # dk.determine_statistical_distributions() - # if self.feature_parameters["remove_outliers"]: - # dk.remove_outliers(predict=False) - def data_cleaning_predict(self, dk: FreqaiDataKitchen, dataframe: DataFrame) -> None: """ Base data cleaning method for predict. @@ -411,11 +396,6 @@ class IFreqaiModel(ABC): if self.freqai_info.get("feature_parameters", {}).get("DI_threshold", 0): dk.check_if_pred_in_training_spaces() - # if self.feature_parameters["determine_statistical_distributions"]: - # dk.determine_statistical_distributions() - # if self.feature_parameters["remove_outliers"]: - # dk.remove_outliers(predict=True) # creates dropped index - def model_exists( self, pair: str, @@ -428,6 +408,8 @@ class IFreqaiModel(ABC): Given a pair and path, check if a model already exists :param pair: pair e.g. BTC/USD :param path: path to model + :return: + :boolean: whether the model file exists or not. """ coin, _ = pair.split("/") @@ -452,16 +434,6 @@ class IFreqaiModel(ABC): Path(self.full_path, Path(self.config["config_files"][0]).name), ) - def remove_features_from_df(self, dataframe: DataFrame) -> DataFrame: - """ - Remove the features from the dataframe before returning it to strategy. This keeps it - compact for Frequi purposes. - """ - to_keep = [ - col for col in dataframe.columns if not col.startswith("%") or col.startswith("%%") - ] - return dataframe[to_keep] - def train_model_in_series( self, new_trained_timerange: TimeRange, @@ -507,7 +479,6 @@ class IFreqaiModel(ABC): if self.freqai_info.get("purge_old_models", False): self.dd.purge_old_models() - # self.retrain = False def set_initial_historic_predictions( self, df: DataFrame, model: Any, dk: FreqaiDataKitchen, pair: str @@ -567,16 +538,6 @@ class IFreqaiModel(ABC): data (NaNs) or felt uncertain about data (i.e. SVM and/or DI index) """ - def make_labels(self, dataframe: DataFrame, dk: FreqaiDataKitchen) -> DataFrame: - """ - User defines the labels here (target values). - :params: - dataframe: DataFrame = the full dataframe for the present training period - dk: FreqaiDataKitchen = Data management/analysis tool assoicated to present pair only - """ - - return - @abstractmethod def return_values(self, dataframe: DataFrame, dk: FreqaiDataKitchen) -> DataFrame: """ From 98c8a447b2047e9f4a633aa458a8a4d139ebbf11 Mon Sep 17 00:00:00 2001 From: robcaulk Date: Fri, 22 Jul 2022 12:40:51 +0200 Subject: [PATCH 22/23] add LightGBMPredictionMultiModel --- .../LightGBMPredictionMultiModel.py | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 freqtrade/freqai/prediction_models/LightGBMPredictionMultiModel.py diff --git a/freqtrade/freqai/prediction_models/LightGBMPredictionMultiModel.py b/freqtrade/freqai/prediction_models/LightGBMPredictionMultiModel.py new file mode 100644 index 000000000..89aad4323 --- /dev/null +++ b/freqtrade/freqai/prediction_models/LightGBMPredictionMultiModel.py @@ -0,0 +1,40 @@ +import logging +from typing import Any, Dict + +from lightgbm import LGBMRegressor +from sklearn.multioutput import MultiOutputRegressor + +from freqtrade.freqai.prediction_models.BaseRegressionModel import BaseRegressionModel + + +logger = logging.getLogger(__name__) + + +class LightGBMPredictionMultiModel(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) -> Any: + """ + User sets up the training and test data to fit their desired model here + :params: + :data_dictionary: the dictionary constructed by DataHandler to hold + all the training and test data/labels. + """ + + lgb = LGBMRegressor(**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"] + + 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}") + return model From accc629e327b6c4a611d32d2292fea20925cce7f Mon Sep 17 00:00:00 2001 From: robcaulk Date: Fri, 22 Jul 2022 12:44:43 +0200 Subject: [PATCH 23/23] set separate table sections in doc --- docs/freqai.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/freqai.md b/docs/freqai.md index 488b82e91..dbe5d1893 100644 --- a/docs/freqai.md +++ b/docs/freqai.md @@ -67,6 +67,7 @@ freqtrade backtesting --config config_examples/config_freqai.example.json --stra ## Configuring the bot +### Parameter table The table below will list all configuration parameters available for `FreqAI`. Mandatory parameters are marked as **Required**, which means that they are required to be set in one of the possible ways. @@ -82,7 +83,6 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `live_trained_timestamp` | Useful if user wants to start from models trained during a *backtest*. The timestamp can be located in the `user_data/models` backtesting folder. This is not a commonly used parameter, leave undefined for most applications.
**Datatype:** positive integer. | `fit_live_predictions_candles` | Computes target (label) statistics from prediction data, instead of from the training data set. Number of candles is the number of historical candles it uses to generate the statistics.
**Datatype:** positive integer. | `purge_old_models` | Tell FreqAI to delete obsolete models. Otherwise, all historic models will remain on disk. Defaults to False.
**Datatype:** boolean. -| | **Feature Parameters** | `expiration_hours` | Ask FreqAI to avoid making predictions if a model is more than `expiration_hours` old. Defaults to 0 which means models never expire.
**Datatype:** positive integer. | | **Feature Parameters** | `feature_parameters` | A dictionary containing the parameters used to engineer the feature set. Details and examples shown [here](#building-the-feature-set)
**Datatype:** dictionary. @@ -108,6 +108,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `learning_rate` | A common parameter among regressors which sets the boosting learning rate.
**Datatype:** float. | `n_jobs`, `thread_count`, `task_type` | Different libraries use different parameter names to control the number of threads used for parallel processing or whether or not it is a `task_type` of `gpu` or `cpu`.
**Datatype:** float. +### Return values for use in strategy Here are the values you can expect to receive inside the dataframe returned by FreqAI: | Parameter | Description |