From 47056eded3429a328b8b92d339738fca60e73818 Mon Sep 17 00:00:00 2001 From: Mark Regan Date: Tue, 25 Oct 2022 18:24:27 +0100 Subject: [PATCH 01/11] multi target classifier working but not for parallel --- .../FreqaiMultiOutputClassifier.py | 64 +++++++++++++++++++ .../CatboostClassifierMultiTarget.py | 55 ++++++++++++++++ 2 files changed, 119 insertions(+) create mode 100644 freqtrade/freqai/base_models/FreqaiMultiOutputClassifier.py create mode 100644 freqtrade/freqai/prediction_models/CatboostClassifierMultiTarget.py diff --git a/freqtrade/freqai/base_models/FreqaiMultiOutputClassifier.py b/freqtrade/freqai/base_models/FreqaiMultiOutputClassifier.py new file mode 100644 index 000000000..54136d5e0 --- /dev/null +++ b/freqtrade/freqai/base_models/FreqaiMultiOutputClassifier.py @@ -0,0 +1,64 @@ +from joblib import Parallel +from sklearn.multioutput import MultiOutputRegressor, _fit_estimator +from sklearn.utils.fixes import delayed +from sklearn.utils.validation import has_fit_parameter + + +class FreqaiMultiOutputRegressor(MultiOutputRegressor): + + def fit(self, X, y, sample_weight=None, fit_params=None): + """Fit the model to data, separately for each output variable. + Parameters + ---------- + X : {array-like, sparse matrix} of shape (n_samples, n_features) + The input data. + y : {array-like, sparse matrix} of shape (n_samples, n_outputs) + Multi-output targets. An indicator matrix turns on multilabel + estimation. + sample_weight : array-like of shape (n_samples,), default=None + Sample weights. If `None`, then samples are equally weighted. + Only supported if the underlying regressor supports sample + weights. + fit_params : A list of dicts for the fit_params + Parameters passed to the ``estimator.fit`` method of each step. + Each dict may contain same or different values (e.g. different + eval_sets or init_models) + .. versionadded:: 0.23 + Returns + ------- + self : object + Returns a fitted instance. + """ + + if not hasattr(self.estimator, "fit"): + raise ValueError("The base estimator should implement a fit method") + + y = self._validate_data(X="no_validation", y=y, multi_output=True) + + if y.ndim == 1: + raise ValueError( + "y must have at least two dimensions for " + "multi-output regression but has only one." + ) + + if sample_weight is not None and not has_fit_parameter( + self.estimator, "sample_weight" + ): + raise ValueError("Underlying estimator does not support sample weights.") + + if not fit_params: + fit_params = [None] * y.shape[1] + + self.estimators_ = Parallel(n_jobs=self.n_jobs)( + delayed(_fit_estimator)( + self.estimator, X, y[:, i], sample_weight, **fit_params[i] + ) + for i in range(y.shape[1]) + ) + + if hasattr(self.estimators_[0], "n_features_in_"): + self.n_features_in_ = self.estimators_[0].n_features_in_ + if hasattr(self.estimators_[0], "feature_names_in_"): + self.feature_names_in_ = self.estimators_[0].feature_names_in_ + + return diff --git a/freqtrade/freqai/prediction_models/CatboostClassifierMultiTarget.py b/freqtrade/freqai/prediction_models/CatboostClassifierMultiTarget.py new file mode 100644 index 000000000..ca1d8ece0 --- /dev/null +++ b/freqtrade/freqai/prediction_models/CatboostClassifierMultiTarget.py @@ -0,0 +1,55 @@ +import logging +import sys +from pathlib import Path +from typing import Any, Dict + +from catboost import CatBoostClassifier, Pool + +from freqtrade.freqai.base_models.BaseClassifierModel import BaseClassifierModel +from freqtrade.freqai.data_kitchen import FreqaiDataKitchen + + +logger = logging.getLogger(__name__) + + +class CatboostClassifier(BaseClassifierModel): + """ + User created prediction model. The class needs to override three necessary + functions, predict(), train(), fit(). The class inherits ModelHandler which + has its own DataHandler where data is held, saved, loaded, and managed. + """ + + def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: + """ + User sets up the training and test data to fit their desired model here + :param data_dictionary: the dictionary constructed by DataHandler to hold + all the training and test data/labels. + """ + + train_data = Pool( + data=data_dictionary["train_features"], + label=data_dictionary["train_labels"], + weight=data_dictionary["train_weights"], + ) + if self.freqai_info.get("data_split_parameters", {}).get("test_size", 0.1) == 0: + test_data = None + else: + test_data = Pool( + data=data_dictionary["test_features"], + label=data_dictionary["test_labels"], + weight=data_dictionary["test_weights"], + ) + + cbr = CatBoostClassifier( + allow_writing_files=True, + loss_function='MultiClass', + train_dir=Path(dk.data_path), + **self.model_training_parameters, + ) + + init_model = self.get_init_model(dk.pair) + + cbr.fit(X=train_data, eval_set=test_data, init_model=init_model, + log_cout=sys.stdout, log_cerr=sys.stderr) + + return cbr From 217add70bd010cae584db5aa13a7d5e76011e2bd Mon Sep 17 00:00:00 2001 From: Mark Regan Date: Tue, 25 Oct 2022 20:07:39 +0100 Subject: [PATCH 02/11] add strat and config for testing on PR --- .../FreqaiMultiOutputClassifier.py | 73 +++++- .../CatboostClassifierMultiTarget.py | 57 ++-- .../MultiTargetClassifierTestStrategy.py | 244 ++++++++++++++++++ user_data/strategies/config_test.json | 105 ++++++++ 4 files changed, 455 insertions(+), 24 deletions(-) create mode 100644 user_data/strategies/MultiTargetClassifierTestStrategy.py create mode 100644 user_data/strategies/config_test.json diff --git a/freqtrade/freqai/base_models/FreqaiMultiOutputClassifier.py b/freqtrade/freqai/base_models/FreqaiMultiOutputClassifier.py index 54136d5e0..a4a8ddfcb 100644 --- a/freqtrade/freqai/base_models/FreqaiMultiOutputClassifier.py +++ b/freqtrade/freqai/base_models/FreqaiMultiOutputClassifier.py @@ -1,10 +1,13 @@ +import numpy as np from joblib import Parallel -from sklearn.multioutput import MultiOutputRegressor, _fit_estimator +from sklearn.base import is_classifier +from sklearn.multioutput import MultiOutputClassifier, _fit_estimator from sklearn.utils.fixes import delayed -from sklearn.utils.validation import has_fit_parameter +from sklearn.utils.multiclass import check_classification_targets +from sklearn.utils.validation import check_is_fitted, has_fit_parameter -class FreqaiMultiOutputRegressor(MultiOutputRegressor): +class FreqaiMultiOutputClassifier(MultiOutputClassifier): def fit(self, X, y, sample_weight=None, fit_params=None): """Fit the model to data, separately for each output variable. @@ -17,7 +20,7 @@ class FreqaiMultiOutputRegressor(MultiOutputRegressor): estimation. sample_weight : array-like of shape (n_samples,), default=None Sample weights. If `None`, then samples are equally weighted. - Only supported if the underlying regressor supports sample + Only supported if the underlying classifier supports sample weights. fit_params : A list of dicts for the fit_params Parameters passed to the ``estimator.fit`` method of each step. @@ -35,6 +38,9 @@ class FreqaiMultiOutputRegressor(MultiOutputRegressor): y = self._validate_data(X="no_validation", y=y, multi_output=True) + if is_classifier(self): + check_classification_targets(y) + if y.ndim == 1: raise ValueError( "y must have at least two dimensions for " @@ -56,9 +62,66 @@ class FreqaiMultiOutputRegressor(MultiOutputRegressor): for i in range(y.shape[1]) ) + self.classes_ = [] + for estimator in self.estimators_: + self.classes_.extend(estimator.classes_) + if hasattr(self.estimators_[0], "n_features_in_"): self.n_features_in_ = self.estimators_[0].n_features_in_ if hasattr(self.estimators_[0], "feature_names_in_"): self.feature_names_in_ = self.estimators_[0].feature_names_in_ - return + return self + + def predict_proba(self, X): + """Return prediction probabilities for each class of each output. + + This method will raise a ``ValueError`` if any of the + estimators do not have ``predict_proba``. + + Parameters + ---------- + X : array-like of shape (n_samples, n_features) + The input data. + + Returns + ------- + p : array of shape (n_samples, n_classes), or a list of n_outputs \ + such arrays if n_outputs > 1. + The class probabilities of the input samples. The order of the + classes corresponds to that in the attribute :term:`classes_`. + + .. versionchanged:: 0.19 + This function now returns a list of arrays where the length of + the list is ``n_outputs``, and each array is (``n_samples``, + ``n_classes``) for that particular output. + """ + check_is_fitted(self) + results = np.hstack([estimator.predict_proba(X) for estimator in self.estimators_]) + return np.squeeze(results) + + def predict(self, X): + """Predict multi-output variable using model for each target variable. + + Parameters + ---------- + X : {array-like, sparse matrix} of shape (n_samples, n_features) + The input data. + + Returns + ------- + y : {array-like, sparse matrix} of shape (n_samples, n_outputs) + Multi-output targets predicted across multiple predictors. + Note: Separate models are generated for each predictor. + """ + check_is_fitted(self) + if not hasattr(self.estimators_[0], "predict"): + raise ValueError("The base estimator should implement a predict method") + + y = Parallel(n_jobs=self.n_jobs)( + delayed(e.predict)(X) for e in self.estimators_ + ) + + results = np.asarray(y).T + + return np.squeeze(results) diff --git a/freqtrade/freqai/prediction_models/CatboostClassifierMultiTarget.py b/freqtrade/freqai/prediction_models/CatboostClassifierMultiTarget.py index ca1d8ece0..c6f900fad 100644 --- a/freqtrade/freqai/prediction_models/CatboostClassifierMultiTarget.py +++ b/freqtrade/freqai/prediction_models/CatboostClassifierMultiTarget.py @@ -6,13 +6,14 @@ from typing import Any, Dict from catboost import CatBoostClassifier, Pool from freqtrade.freqai.base_models.BaseClassifierModel import BaseClassifierModel +from freqtrade.freqai.base_models.FreqaiMultiOutputClassifier import FreqaiMultiOutputClassifier from freqtrade.freqai.data_kitchen import FreqaiDataKitchen logger = logging.getLogger(__name__) -class CatboostClassifier(BaseClassifierModel): +class CatboostClassifierMultiTarget(BaseClassifierModel): """ User created prediction model. The class needs to override three necessary functions, predict(), train(), fit(). The class inherits ModelHandler which @@ -26,30 +27,48 @@ class CatboostClassifier(BaseClassifierModel): all the training and test data/labels. """ - train_data = Pool( - data=data_dictionary["train_features"], - label=data_dictionary["train_labels"], - weight=data_dictionary["train_weights"], - ) - if self.freqai_info.get("data_split_parameters", {}).get("test_size", 0.1) == 0: - test_data = None - else: - test_data = Pool( - data=data_dictionary["test_features"], - label=data_dictionary["test_labels"], - weight=data_dictionary["test_weights"], - ) - - cbr = CatBoostClassifier( + cbc = CatBoostClassifier( allow_writing_files=True, loss_function='MultiClass', train_dir=Path(dk.data_path), **self.model_training_parameters, ) + X = data_dictionary["train_features"] + y = data_dictionary["train_labels"] + + sample_weight = data_dictionary["train_weights"] + + eval_sets = [None] * y.shape[1] + + if self.freqai_info.get('data_split_parameters', {}).get('test_size', 0.1) != 0: + eval_sets = [None] * data_dictionary['test_labels'].shape[1] + + for i in range(data_dictionary['test_labels'].shape[1]): + eval_sets[i] = Pool( + data=data_dictionary["test_features"], + label=data_dictionary["test_labels"].iloc[:, i], + weight=data_dictionary["test_weights"], + ) + init_model = self.get_init_model(dk.pair) - cbr.fit(X=train_data, eval_set=test_data, init_model=init_model, - log_cout=sys.stdout, log_cerr=sys.stderr) + if init_model: + init_models = init_model.estimators_ + else: + init_models = [None] * y.shape[1] - return cbr + fit_params = [] + for i in range(len(eval_sets)): + fit_params.append({ + 'eval_set': eval_sets[i], 'init_model': init_models[i], + 'log_cout': sys.stdout, 'log_cerr': sys.stderr, + }) + + model = FreqaiMultiOutputClassifier(estimator=cbc) + thread_training = self.freqai_info.get('multitarget_parallel_training', False) + if thread_training: + model.n_jobs = y.shape[1] + model.fit(X=X, y=y, sample_weight=sample_weight, fit_params=fit_params) + + return model diff --git a/user_data/strategies/MultiTargetClassifierTestStrategy.py b/user_data/strategies/MultiTargetClassifierTestStrategy.py new file mode 100644 index 000000000..6ca2567c3 --- /dev/null +++ b/user_data/strategies/MultiTargetClassifierTestStrategy.py @@ -0,0 +1,244 @@ +import logging +from functools import reduce + +import numpy as np +import pandas as pd +import talib.abstract as ta +from pandas import DataFrame +from technical import qtpylib + +from freqtrade.strategy import CategoricalParameter, IStrategy, merge_informative_pair + + +logger = logging.getLogger(__name__) + + +class MultiTargetClassifierTestStrategy(IStrategy): + """ + Example strategy showing how the user connects their own + IFreqaiModel to the strategy. Namely, the user uses: + self.freqai.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"}}, + "do_predict": { + "do_predict": {"color": "brown"}, + }, + }, + } + + process_only_new_candles = True + stoploss = -0.05 + use_exit_signal = True + # this is the maximum period fed to talib (timeframe independent) + startup_candle_count: int = 40 + can_short = False + + std_dev_multiplier_buy = CategoricalParameter( + [0.75, 1, 1.25, 1.5, 1.75], default=1.25, space="buy", optimize=True) + std_dev_multiplier_sell = CategoricalParameter( + [0.75, 1, 1.25, 1.5, 1.75], space="sell", default=1.25, optimize=True) + + def populate_any_indicators( + self, pair, df, tf, informative=None, set_generalized_indicators=False + ): + """ + Function designed to automatically generate, name and merge features + from user indicated timeframes in the configuration file. User controls the indicators + passed to the training/prediction by prepending indicators with `'%-' + coin ` + (see convention below). I.e. user should not prepend any supporting metrics + (e.g. bb_lowerband below) with % unless they explicitly want to pass that metric to the + model. + :param pair: pair to be used as informative + :param df: strategy dataframe which will receive merges from informatives + :param tf: timeframe of the dataframe which will modify the feature names + :param informative: the dataframe associated with the informative pair + """ + + coin = pair.split('/')[0] + + if informative is None: + informative = self.dp.get_pair_dataframe(pair, tf) + + # first loop is automatically duplicating indicators for time periods + for t in self.freqai_info["feature_parameters"]["indicator_periods_candles"]: + + t = int(t) + informative[f"%-{coin}rsi-period_{t}"] = ta.RSI(informative, timeperiod=t) + informative[f"%-{coin}mfi-period_{t}"] = ta.MFI(informative, timeperiod=t) + informative[f"%-{coin}adx-period_{t}"] = ta.ADX(informative, timeperiod=t) + informative[f"%-{coin}sma-period_{t}"] = ta.SMA(informative, timeperiod=t) + informative[f"%-{coin}ema-period_{t}"] = ta.EMA(informative, timeperiod=t) + + bollinger = qtpylib.bollinger_bands( + qtpylib.typical_price(informative), window=t, stds=2.2 + ) + informative[f"{coin}bb_lowerband-period_{t}"] = bollinger["lower"] + informative[f"{coin}bb_middleband-period_{t}"] = bollinger["mid"] + informative[f"{coin}bb_upperband-period_{t}"] = bollinger["upper"] + + informative[f"%-{coin}bb_width-period_{t}"] = ( + informative[f"{coin}bb_upperband-period_{t}"] + - informative[f"{coin}bb_lowerband-period_{t}"] + ) / informative[f"{coin}bb_middleband-period_{t}"] + informative[f"%-{coin}close-bb_lower-period_{t}"] = ( + informative["close"] / informative[f"{coin}bb_lowerband-period_{t}"] + ) + + informative[f"%-{coin}roc-period_{t}"] = ta.ROC(informative, timeperiod=t) + + informative[f"%-{coin}relative_volume-period_{t}"] = ( + informative["volume"] / informative["volume"].rolling(t).mean() + ) + + informative[f"%-{coin}pct-change"] = informative["close"].pct_change() + informative[f"%-{coin}raw_volume"] = informative["volume"] + informative[f"%-{coin}raw_price"] = informative["close"] + + indicators = [col for col in informative if col.startswith("%")] + # This loop duplicates and shifts all indicators to add a sense of recency to data + for n in range(self.freqai_info["feature_parameters"]["include_shifted_candles"] + 1): + if n == 0: + continue + informative_shift = informative[indicators].shift(n) + informative_shift = informative_shift.add_suffix("_shift-" + str(n)) + informative = pd.concat((informative, informative_shift), axis=1) + + 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 + + # Classifiers are typically set up with strings as targets: + df['&s-up_or_down_long'] = np.where( + df["close"].shift(-100) > df["close"], 'up_long', 'down_long') + df['&s-up_or_down_medium'] = np.where( + df["close"].shift(-50) > df["close"], 'up_medium', 'down_medium') + df['&s-up_or_down_short'] = np.where( + df["close"].shift(-20) > df["close"], 'up_short', 'down_short') + + # If user wishes to use multiple targets, they can add more by + # appending more columns with '&'. User should keep in mind that multi targets + # requires a multioutput prediction model such as + # templates/CatboostPredictionMultiModel.py, + + # df["&-s_range"] = ( + # df["close"] + # .shift(-self.freqai_info["feature_parameters"]["label_period_candles"]) + # .rolling(self.freqai_info["feature_parameters"]["label_period_candles"]) + # .max() + # - + # df["close"] + # .shift(-self.freqai_info["feature_parameters"]["label_period_candles"]) + # .rolling(self.freqai_info["feature_parameters"]["label_period_candles"]) + # .min() + # ) + + return df + + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + + # All indicators must be populated by populate_any_indicators() for live functionality + # to work correctly. + + # 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.freqai.start(dataframe, metadata, self) + for val in self.std_dev_multiplier_buy.range: + dataframe[f'target_roi_{val}'] = ( + dataframe["up_long_mean"] + dataframe["up_long_std"] * val + ) + for val in self.std_dev_multiplier_sell.range: + dataframe[f'sell_roi_{val}'] = ( + dataframe["down_long_mean"] - dataframe["down_long_std"] * val + ) + return dataframe + + def populate_entry_trend(self, df: DataFrame, metadata: dict) -> DataFrame: + + enter_long_conditions = [ + df["do_predict"] == 1, + df["up_long"] > df[f"target_roi_{self.std_dev_multiplier_buy.value}"], + ] + + if enter_long_conditions: + df.loc[ + reduce(lambda x, y: x & y, enter_long_conditions), ["enter_long", "enter_tag"] + ] = (1, "long") + + enter_short_conditions = [ + df["do_predict"] == 1, + df["down_long"] < df[f"sell_roi_{self.std_dev_multiplier_sell.value}"], + ] + + 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["down_long"] < df[f"sell_roi_{self.std_dev_multiplier_sell.value}"] * 0.25, + ] + if exit_long_conditions: + df.loc[reduce(lambda x, y: x & y, exit_long_conditions), "exit_long"] = 1 + + exit_short_conditions = [ + df["do_predict"] == 1, + df["up_long"] > df[f"target_roi_{self.std_dev_multiplier_buy.value}"] * 0.25, + ] + if exit_short_conditions: + df.loc[reduce(lambda x, y: x & y, exit_short_conditions), "exit_short"] = 1 + + return df + + def get_ticker_indicator(self): + return int(self.config["timeframe"][:-1]) + + def confirm_trade_entry( + self, + pair: str, + order_type: str, + amount: float, + rate: float, + time_in_force: str, + current_time, + entry_tag, + side: str, + **kwargs, + ) -> bool: + + df, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) + last_candle = df.iloc[-1].squeeze() + + if side == "long": + if rate > (last_candle["close"] * (1 + 0.0025)): + return False + else: + if rate < (last_candle["close"] * (1 - 0.0025)): + return False + + return True diff --git a/user_data/strategies/config_test.json b/user_data/strategies/config_test.json new file mode 100644 index 000000000..5e508096d --- /dev/null +++ b/user_data/strategies/config_test.json @@ -0,0 +1,105 @@ +{ + "trading_mode": "futures", + "margin_mode": "isolated", + "max_open_trades": 5, + "stake_currency": "USDT", + "stake_amount": 200, + "tradable_balance_ratio": 1, + "fiat_display_currency": "USD", + "dry_run": true, + "timeframe": "3m", + "dry_run_wallet": 1000, + "cancel_open_orders_on_exit": true, + "unfilledtimeout": { + "entry": 10, + "exit": 30 + }, + "exchange": { + "name": "binance", + "key": "", + "secret": "", + "ccxt_config": {}, + "ccxt_async_config": {}, + "pair_whitelist": [ + "1INCH/USDT", + "ALGO/USDT" + ], + "pair_blacklist": [] + }, + "entry_pricing": { + "price_side": "same", + "use_order_book": true, + "order_book_top": 1, + "price_last_balance": 0.0, + "check_depth_of_market": { + "enabled": false, + "bids_to_ask_delta": 1 + } + }, + "exit_pricing": { + "price_side": "other", + "use_order_book": true, + "order_book_top": 1 + }, + "pairlists": [ + { + "method": "StaticPairList" + } + ], + "freqai": { + "enabled": true, + "purge_old_models": true, + "train_period_days": 15, + "backtest_period_days": 7, + "live_retrain_hours": 0, + "identifier": "uniqe-id", + "multitarget_parallel_training": true, + "feature_parameters": { + "include_timeframes": [ + "3m", + "15m", + "1h" + ], + "include_corr_pairlist": [ + "BTC/USDT", + "ETH/USDT" + ], + "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, + "indicator_periods_candles": [ + 10, + 20 + ], + "plot_feature_importances": 0 + }, + "data_split_parameters": { + "test_size": 0.33, + "random_state": 1 + }, + "model_training_parameters": { + "n_estimators": 1000, + "early_stopping_rounds": 100 + } + }, + "api_server": { + "enabled": true, + "listen_ip_address": "127.0.0.1", + "listen_port": 8081, + "verbosity": "error", + "enable_openapi": false, + "jwt_secret_key": "test", + "CORS_origins": [], + "username": "test", + "password": "test" + }, + "bot_name": "", + "force_entry_enable": true, + "initial_state": "running", + "internals": { + "process_throttle_secs": 5 + } +} From a9a3ceadf753d5b8c9ea23cd13ad3c71a52e6972 Mon Sep 17 00:00:00 2001 From: Mark Regan Date: Wed, 26 Oct 2022 13:10:18 +0100 Subject: [PATCH 03/11] Delete config_test.json --- user_data/strategies/config_test.json | 105 -------------------------- 1 file changed, 105 deletions(-) delete mode 100644 user_data/strategies/config_test.json diff --git a/user_data/strategies/config_test.json b/user_data/strategies/config_test.json deleted file mode 100644 index 5e508096d..000000000 --- a/user_data/strategies/config_test.json +++ /dev/null @@ -1,105 +0,0 @@ -{ - "trading_mode": "futures", - "margin_mode": "isolated", - "max_open_trades": 5, - "stake_currency": "USDT", - "stake_amount": 200, - "tradable_balance_ratio": 1, - "fiat_display_currency": "USD", - "dry_run": true, - "timeframe": "3m", - "dry_run_wallet": 1000, - "cancel_open_orders_on_exit": true, - "unfilledtimeout": { - "entry": 10, - "exit": 30 - }, - "exchange": { - "name": "binance", - "key": "", - "secret": "", - "ccxt_config": {}, - "ccxt_async_config": {}, - "pair_whitelist": [ - "1INCH/USDT", - "ALGO/USDT" - ], - "pair_blacklist": [] - }, - "entry_pricing": { - "price_side": "same", - "use_order_book": true, - "order_book_top": 1, - "price_last_balance": 0.0, - "check_depth_of_market": { - "enabled": false, - "bids_to_ask_delta": 1 - } - }, - "exit_pricing": { - "price_side": "other", - "use_order_book": true, - "order_book_top": 1 - }, - "pairlists": [ - { - "method": "StaticPairList" - } - ], - "freqai": { - "enabled": true, - "purge_old_models": true, - "train_period_days": 15, - "backtest_period_days": 7, - "live_retrain_hours": 0, - "identifier": "uniqe-id", - "multitarget_parallel_training": true, - "feature_parameters": { - "include_timeframes": [ - "3m", - "15m", - "1h" - ], - "include_corr_pairlist": [ - "BTC/USDT", - "ETH/USDT" - ], - "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, - "indicator_periods_candles": [ - 10, - 20 - ], - "plot_feature_importances": 0 - }, - "data_split_parameters": { - "test_size": 0.33, - "random_state": 1 - }, - "model_training_parameters": { - "n_estimators": 1000, - "early_stopping_rounds": 100 - } - }, - "api_server": { - "enabled": true, - "listen_ip_address": "127.0.0.1", - "listen_port": 8081, - "verbosity": "error", - "enable_openapi": false, - "jwt_secret_key": "test", - "CORS_origins": [], - "username": "test", - "password": "test" - }, - "bot_name": "", - "force_entry_enable": true, - "initial_state": "running", - "internals": { - "process_throttle_secs": 5 - } -} From 1c98640129cc44337da32f1488d5faae8211c70f Mon Sep 17 00:00:00 2001 From: Mark Regan Date: Wed, 26 Oct 2022 13:11:10 +0100 Subject: [PATCH 04/11] Delete MultiTargetClassifierTestStrategy.py --- .../MultiTargetClassifierTestStrategy.py | 244 ------------------ 1 file changed, 244 deletions(-) delete mode 100644 user_data/strategies/MultiTargetClassifierTestStrategy.py diff --git a/user_data/strategies/MultiTargetClassifierTestStrategy.py b/user_data/strategies/MultiTargetClassifierTestStrategy.py deleted file mode 100644 index 6ca2567c3..000000000 --- a/user_data/strategies/MultiTargetClassifierTestStrategy.py +++ /dev/null @@ -1,244 +0,0 @@ -import logging -from functools import reduce - -import numpy as np -import pandas as pd -import talib.abstract as ta -from pandas import DataFrame -from technical import qtpylib - -from freqtrade.strategy import CategoricalParameter, IStrategy, merge_informative_pair - - -logger = logging.getLogger(__name__) - - -class MultiTargetClassifierTestStrategy(IStrategy): - """ - Example strategy showing how the user connects their own - IFreqaiModel to the strategy. Namely, the user uses: - self.freqai.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"}}, - "do_predict": { - "do_predict": {"color": "brown"}, - }, - }, - } - - process_only_new_candles = True - stoploss = -0.05 - use_exit_signal = True - # this is the maximum period fed to talib (timeframe independent) - startup_candle_count: int = 40 - can_short = False - - std_dev_multiplier_buy = CategoricalParameter( - [0.75, 1, 1.25, 1.5, 1.75], default=1.25, space="buy", optimize=True) - std_dev_multiplier_sell = CategoricalParameter( - [0.75, 1, 1.25, 1.5, 1.75], space="sell", default=1.25, optimize=True) - - def populate_any_indicators( - self, pair, df, tf, informative=None, set_generalized_indicators=False - ): - """ - Function designed to automatically generate, name and merge features - from user indicated timeframes in the configuration file. User controls the indicators - passed to the training/prediction by prepending indicators with `'%-' + coin ` - (see convention below). I.e. user should not prepend any supporting metrics - (e.g. bb_lowerband below) with % unless they explicitly want to pass that metric to the - model. - :param pair: pair to be used as informative - :param df: strategy dataframe which will receive merges from informatives - :param tf: timeframe of the dataframe which will modify the feature names - :param informative: the dataframe associated with the informative pair - """ - - coin = pair.split('/')[0] - - if informative is None: - informative = self.dp.get_pair_dataframe(pair, tf) - - # first loop is automatically duplicating indicators for time periods - for t in self.freqai_info["feature_parameters"]["indicator_periods_candles"]: - - t = int(t) - informative[f"%-{coin}rsi-period_{t}"] = ta.RSI(informative, timeperiod=t) - informative[f"%-{coin}mfi-period_{t}"] = ta.MFI(informative, timeperiod=t) - informative[f"%-{coin}adx-period_{t}"] = ta.ADX(informative, timeperiod=t) - informative[f"%-{coin}sma-period_{t}"] = ta.SMA(informative, timeperiod=t) - informative[f"%-{coin}ema-period_{t}"] = ta.EMA(informative, timeperiod=t) - - bollinger = qtpylib.bollinger_bands( - qtpylib.typical_price(informative), window=t, stds=2.2 - ) - informative[f"{coin}bb_lowerband-period_{t}"] = bollinger["lower"] - informative[f"{coin}bb_middleband-period_{t}"] = bollinger["mid"] - informative[f"{coin}bb_upperband-period_{t}"] = bollinger["upper"] - - informative[f"%-{coin}bb_width-period_{t}"] = ( - informative[f"{coin}bb_upperband-period_{t}"] - - informative[f"{coin}bb_lowerband-period_{t}"] - ) / informative[f"{coin}bb_middleband-period_{t}"] - informative[f"%-{coin}close-bb_lower-period_{t}"] = ( - informative["close"] / informative[f"{coin}bb_lowerband-period_{t}"] - ) - - informative[f"%-{coin}roc-period_{t}"] = ta.ROC(informative, timeperiod=t) - - informative[f"%-{coin}relative_volume-period_{t}"] = ( - informative["volume"] / informative["volume"].rolling(t).mean() - ) - - informative[f"%-{coin}pct-change"] = informative["close"].pct_change() - informative[f"%-{coin}raw_volume"] = informative["volume"] - informative[f"%-{coin}raw_price"] = informative["close"] - - indicators = [col for col in informative if col.startswith("%")] - # This loop duplicates and shifts all indicators to add a sense of recency to data - for n in range(self.freqai_info["feature_parameters"]["include_shifted_candles"] + 1): - if n == 0: - continue - informative_shift = informative[indicators].shift(n) - informative_shift = informative_shift.add_suffix("_shift-" + str(n)) - informative = pd.concat((informative, informative_shift), axis=1) - - 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 - - # Classifiers are typically set up with strings as targets: - df['&s-up_or_down_long'] = np.where( - df["close"].shift(-100) > df["close"], 'up_long', 'down_long') - df['&s-up_or_down_medium'] = np.where( - df["close"].shift(-50) > df["close"], 'up_medium', 'down_medium') - df['&s-up_or_down_short'] = np.where( - df["close"].shift(-20) > df["close"], 'up_short', 'down_short') - - # If user wishes to use multiple targets, they can add more by - # appending more columns with '&'. User should keep in mind that multi targets - # requires a multioutput prediction model such as - # templates/CatboostPredictionMultiModel.py, - - # df["&-s_range"] = ( - # df["close"] - # .shift(-self.freqai_info["feature_parameters"]["label_period_candles"]) - # .rolling(self.freqai_info["feature_parameters"]["label_period_candles"]) - # .max() - # - - # df["close"] - # .shift(-self.freqai_info["feature_parameters"]["label_period_candles"]) - # .rolling(self.freqai_info["feature_parameters"]["label_period_candles"]) - # .min() - # ) - - return df - - def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: - - # All indicators must be populated by populate_any_indicators() for live functionality - # to work correctly. - - # 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.freqai.start(dataframe, metadata, self) - for val in self.std_dev_multiplier_buy.range: - dataframe[f'target_roi_{val}'] = ( - dataframe["up_long_mean"] + dataframe["up_long_std"] * val - ) - for val in self.std_dev_multiplier_sell.range: - dataframe[f'sell_roi_{val}'] = ( - dataframe["down_long_mean"] - dataframe["down_long_std"] * val - ) - return dataframe - - def populate_entry_trend(self, df: DataFrame, metadata: dict) -> DataFrame: - - enter_long_conditions = [ - df["do_predict"] == 1, - df["up_long"] > df[f"target_roi_{self.std_dev_multiplier_buy.value}"], - ] - - if enter_long_conditions: - df.loc[ - reduce(lambda x, y: x & y, enter_long_conditions), ["enter_long", "enter_tag"] - ] = (1, "long") - - enter_short_conditions = [ - df["do_predict"] == 1, - df["down_long"] < df[f"sell_roi_{self.std_dev_multiplier_sell.value}"], - ] - - 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["down_long"] < df[f"sell_roi_{self.std_dev_multiplier_sell.value}"] * 0.25, - ] - if exit_long_conditions: - df.loc[reduce(lambda x, y: x & y, exit_long_conditions), "exit_long"] = 1 - - exit_short_conditions = [ - df["do_predict"] == 1, - df["up_long"] > df[f"target_roi_{self.std_dev_multiplier_buy.value}"] * 0.25, - ] - if exit_short_conditions: - df.loc[reduce(lambda x, y: x & y, exit_short_conditions), "exit_short"] = 1 - - return df - - def get_ticker_indicator(self): - return int(self.config["timeframe"][:-1]) - - def confirm_trade_entry( - self, - pair: str, - order_type: str, - amount: float, - rate: float, - time_in_force: str, - current_time, - entry_tag, - side: str, - **kwargs, - ) -> bool: - - df, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) - last_candle = df.iloc[-1].squeeze() - - if side == "long": - if rate > (last_candle["close"] * (1 + 0.0025)): - return False - else: - if rate < (last_candle["close"] * (1 - 0.0025)): - return False - - return True From 6ef82dd8b6b92c0e0eb02e47d115f3272082fec6 Mon Sep 17 00:00:00 2001 From: Mark Regan Date: Thu, 27 Oct 2022 12:41:12 +0100 Subject: [PATCH 05/11] minor change to return --- .../freqai/base_models/FreqaiMultiOutputClassifier.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/freqtrade/freqai/base_models/FreqaiMultiOutputClassifier.py b/freqtrade/freqai/base_models/FreqaiMultiOutputClassifier.py index a4a8ddfcb..ce4b6ec84 100644 --- a/freqtrade/freqai/base_models/FreqaiMultiOutputClassifier.py +++ b/freqtrade/freqai/base_models/FreqaiMultiOutputClassifier.py @@ -97,8 +97,10 @@ class FreqaiMultiOutputClassifier(MultiOutputClassifier): ``n_classes``) for that particular output. """ check_is_fitted(self) - results = np.hstack([estimator.predict_proba(X) for estimator in self.estimators_]) - return np.squeeze(results) + results = np.squeeze(np.hstack( + [estimator.predict_proba(X) for estimator in self.estimators_] + )) + return results def predict(self, X): """Predict multi-output variable using model for each target variable. @@ -122,6 +124,6 @@ class FreqaiMultiOutputClassifier(MultiOutputClassifier): delayed(e.predict)(X) for e in self.estimators_ ) - results = np.asarray(y).T + results = np.squeeze(np.asarray(y).T) - return np.squeeze(results) + return results From 7053f81fa882db60118ff97f997e210ff138598c Mon Sep 17 00:00:00 2001 From: Mark Regan Date: Sun, 30 Oct 2022 09:48:30 +0000 Subject: [PATCH 06/11] simplified predict and predict_proba using super(). Added duplicate class label check. --- .../FreqaiMultiOutputClassifier.py | 66 +++++-------------- 1 file changed, 15 insertions(+), 51 deletions(-) diff --git a/freqtrade/freqai/base_models/FreqaiMultiOutputClassifier.py b/freqtrade/freqai/base_models/FreqaiMultiOutputClassifier.py index ce4b6ec84..435c0e646 100644 --- a/freqtrade/freqai/base_models/FreqaiMultiOutputClassifier.py +++ b/freqtrade/freqai/base_models/FreqaiMultiOutputClassifier.py @@ -4,7 +4,9 @@ from sklearn.base import is_classifier from sklearn.multioutput import MultiOutputClassifier, _fit_estimator from sklearn.utils.fixes import delayed from sklearn.utils.multiclass import check_classification_targets -from sklearn.utils.validation import check_is_fitted, has_fit_parameter +from sklearn.utils.validation import has_fit_parameter + +from freqtrade.exceptions import OperationalException class FreqaiMultiOutputClassifier(MultiOutputClassifier): @@ -65,6 +67,9 @@ class FreqaiMultiOutputClassifier(MultiOutputClassifier): self.classes_ = [] for estimator in self.estimators_: self.classes_.extend(estimator.classes_) + if len(set(self.classes_)) != len(self.classes_): + raise OperationalException(f"Class labels must be unique across targets: " + f"{self.classes_}") if hasattr(self.estimators_[0], "n_features_in_"): self.n_features_in_ = self.estimators_[0].n_features_in_ @@ -74,56 +79,15 @@ class FreqaiMultiOutputClassifier(MultiOutputClassifier): return self def predict_proba(self, X): - """Return prediction probabilities for each class of each output. - - This method will raise a ``ValueError`` if any of the - estimators do not have ``predict_proba``. - - Parameters - ---------- - X : array-like of shape (n_samples, n_features) - The input data. - - Returns - ------- - p : array of shape (n_samples, n_classes), or a list of n_outputs \ - such arrays if n_outputs > 1. - The class probabilities of the input samples. The order of the - classes corresponds to that in the attribute :term:`classes_`. - - .. versionchanged:: 0.19 - This function now returns a list of arrays where the length of - the list is ``n_outputs``, and each array is (``n_samples``, - ``n_classes``) for that particular output. - """ - check_is_fitted(self) - results = np.squeeze(np.hstack( - [estimator.predict_proba(X) for estimator in self.estimators_] - )) - return results + """ + Get predict_proba and stack arrays horizontally + """ + results = np.hstack(super().predict_proba(X)) + return np.squeeze(results) def predict(self, X): - """Predict multi-output variable using model for each target variable. - - Parameters - ---------- - X : {array-like, sparse matrix} of shape (n_samples, n_features) - The input data. - - Returns - ------- - y : {array-like, sparse matrix} of shape (n_samples, n_outputs) - Multi-output targets predicted across multiple predictors. - Note: Separate models are generated for each predictor. """ - check_is_fitted(self) - if not hasattr(self.estimators_[0], "predict"): - raise ValueError("The base estimator should implement a predict method") - - y = Parallel(n_jobs=self.n_jobs)( - delayed(e.predict)(X) for e in self.estimators_ - ) - - results = np.squeeze(np.asarray(y).T) - - return results + Get predict and squeeze into 2D array + """ + results = super().predict(X) + return np.squeeze(results) From a49edfbaee004cab8d7aa20cab793fb5a4da1dc3 Mon Sep 17 00:00:00 2001 From: robcaulk Date: Sun, 30 Oct 2022 18:08:10 +0100 Subject: [PATCH 07/11] add tests for CatboostClassifier --- tests/freqai/test_freqai_interface.py | 17 ++- tests/rpc/test_rpc_apiserver.py | 1 + ...freqai_test_multimodel_classifier_strat.py | 138 ++++++++++++++++++ tests/strategy/test_strategy_loading.py | 6 +- 4 files changed, 152 insertions(+), 10 deletions(-) create mode 100644 tests/strategy/strats/freqai_test_multimodel_classifier_strat.py diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index b619c0611..5b9453a4a 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -75,17 +75,20 @@ def test_extract_data_and_train_model_Standard(mocker, freqai_conf, model): shutil.rmtree(Path(freqai.dk.full_path)) -@pytest.mark.parametrize('model', [ - 'LightGBMRegressorMultiTarget', - 'XGBoostRegressorMultiTarget', - 'CatboostRegressorMultiTarget', +@pytest.mark.parametrize('model, strat', [ + ('LightGBMRegressorMultiTarget', "freqai_test_multimodel_strat"), + ('XGBoostRegressorMultiTarget', "freqai_test_multimodel_strat"), + ('CatboostRegressorMultiTarget', "freqai_test_multimodel_strat"), + # ('LightGBMClassifierMultiTarget', "freqai_test_multimodel_classifier_strat"), + # ('XGBoostClassifierMultiTarget', "freqai_test_multimodel_classifier_strat"), + ('CatboostClassifierMultiTarget', "freqai_test_multimodel_classifier_strat") ]) -def test_extract_data_and_train_model_MultiTargets(mocker, freqai_conf, model): - if is_arm() and model == 'CatboostRegressorMultiTarget': +def test_extract_data_and_train_model_MultiTargets(mocker, freqai_conf, model, strat): + if is_arm() and 'Catboost' in model: pytest.skip("CatBoost is not supported on ARM") freqai_conf.update({"timerange": "20180110-20180130"}) - freqai_conf.update({"strategy": "freqai_test_multimodel_strat"}) + freqai_conf.update({"strategy": strat}) freqai_conf.update({"freqaimodel": model}) strategy = get_patched_freqai_strategy(mocker, freqai_conf) exchange = get_patched_exchange(mocker, freqai_conf) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 6c28c1cac..019b8fc82 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -1460,6 +1460,7 @@ def test_api_strategies(botclient, tmpdir): 'StrategyTestV3CustomEntryPrice', 'StrategyTestV3Futures', 'freqai_test_classifier', + 'freqai_test_multimodel_classifier_strat', 'freqai_test_multimodel_strat', 'freqai_test_strat' ]} diff --git a/tests/strategy/strats/freqai_test_multimodel_classifier_strat.py b/tests/strategy/strats/freqai_test_multimodel_classifier_strat.py new file mode 100644 index 000000000..d82737fbb --- /dev/null +++ b/tests/strategy/strats/freqai_test_multimodel_classifier_strat.py @@ -0,0 +1,138 @@ +import logging +from functools import reduce + +import pandas as pd +import talib.abstract as ta +from pandas import DataFrame +import numpy as np + +from freqtrade.strategy import DecimalParameter, IntParameter, IStrategy, merge_informative_pair + + +logger = logging.getLogger(__name__) + + +class freqai_test_multimodel_classifier_strat(IStrategy): + """ + Test strategy - used for testing freqAI multimodel functionalities. + DO not use in production. + """ + + 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 populate_any_indicators( + self, pair, df, tf, informative=None, set_generalized_indicators=False + ): + + coin = pair.split('/')[0] + + if informative is None: + informative = self.dp.get_pair_dataframe(pair, tf) + + # first loop is automatically duplicating indicators for time periods + for t in self.freqai_info["feature_parameters"]["indicator_periods_candles"]: + + t = int(t) + informative[f"%-{coin}rsi-period_{t}"] = ta.RSI(informative, timeperiod=t) + informative[f"%-{coin}mfi-period_{t}"] = ta.MFI(informative, timeperiod=t) + 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-up_or_down'] = np.where(df["close"].shift(-50) > + df["close"], 'up', 'down') + + df['&s-up_or_down2'] = np.where(df["close"].shift(-50) > + df["close"], 'up2', 'down2') + + return df + + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + + self.freqai_info = self.config["freqai"] + + dataframe = self.freqai.start(dataframe, metadata, self) + + dataframe["target_roi"] = dataframe["&-s_close_mean"] + dataframe["&-s_close_std"] * 1.25 + dataframe["sell_roi"] = dataframe["&-s_close_mean"] - dataframe["&-s_close_std"] * 1.25 + return dataframe + + def populate_entry_trend(self, df: DataFrame, metadata: dict) -> DataFrame: + + enter_long_conditions = [df["do_predict"] == 1, df["&-s_close"] > df["target_roi"]] + + if enter_long_conditions: + df.loc[ + reduce(lambda x, y: x & y, enter_long_conditions), ["enter_long", "enter_tag"] + ] = (1, "long") + + enter_short_conditions = [df["do_predict"] == 1, df["&-s_close"] < df["sell_roi"]] + + if enter_short_conditions: + df.loc[ + reduce(lambda x, y: x & y, enter_short_conditions), ["enter_short", "enter_tag"] + ] = (1, "short") + + return df + + def populate_exit_trend(self, df: DataFrame, metadata: dict) -> DataFrame: + exit_long_conditions = [df["do_predict"] == 1, df["&-s_close"] < df["sell_roi"] * 0.25] + if exit_long_conditions: + df.loc[reduce(lambda x, y: x & y, exit_long_conditions), "exit_long"] = 1 + + exit_short_conditions = [df["do_predict"] == 1, df["&-s_close"] > df["target_roi"] * 0.25] + if exit_short_conditions: + df.loc[reduce(lambda x, y: x & y, exit_short_conditions), "exit_short"] = 1 + + return df diff --git a/tests/strategy/test_strategy_loading.py b/tests/strategy/test_strategy_loading.py index 2d13fc380..6b831c116 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) == 10 + assert len(strategies) == 11 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) == 11 + assert len(strategies) == 12 # 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]) == 10 + assert len([x for x in strategies if x['class'] is not None]) == 11 assert len([x for x in strategies if x['class'] is None]) == 1 From 162056a362b51bf58d7a9209a95da88481a2a820 Mon Sep 17 00:00:00 2001 From: robcaulk Date: Mon, 31 Oct 2022 18:23:35 +0100 Subject: [PATCH 08/11] fix flake8 --- .../strategy/strats/freqai_test_multimodel_classifier_strat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/strategy/strats/freqai_test_multimodel_classifier_strat.py b/tests/strategy/strats/freqai_test_multimodel_classifier_strat.py index d82737fbb..421090525 100644 --- a/tests/strategy/strats/freqai_test_multimodel_classifier_strat.py +++ b/tests/strategy/strats/freqai_test_multimodel_classifier_strat.py @@ -94,7 +94,7 @@ class freqai_test_multimodel_classifier_strat(IStrategy): df["close"], 'up', 'down') df['&s-up_or_down2'] = np.where(df["close"].shift(-50) > - df["close"], 'up2', 'down2') + df["close"], 'up2', 'down2') return df From 63458a6130da1591c6b6cbcbfb852ad4e2cd927f Mon Sep 17 00:00:00 2001 From: robcaulk Date: Wed, 2 Nov 2022 18:40:13 +0100 Subject: [PATCH 09/11] isort --- .../strategy/strats/freqai_test_multimodel_classifier_strat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/strategy/strats/freqai_test_multimodel_classifier_strat.py b/tests/strategy/strats/freqai_test_multimodel_classifier_strat.py index 421090525..9188fa331 100644 --- a/tests/strategy/strats/freqai_test_multimodel_classifier_strat.py +++ b/tests/strategy/strats/freqai_test_multimodel_classifier_strat.py @@ -1,10 +1,10 @@ import logging from functools import reduce +import numpy as np import pandas as pd import talib.abstract as ta from pandas import DataFrame -import numpy as np from freqtrade.strategy import DecimalParameter, IntParameter, IStrategy, merge_informative_pair From 054133955b4c67312d053519308b43ed7a680788 Mon Sep 17 00:00:00 2001 From: robcaulk Date: Fri, 11 Nov 2022 17:24:09 +0100 Subject: [PATCH 10/11] fix loading of metric tracker from disk --- freqtrade/freqai/data_drawer.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/freqai/data_drawer.py b/freqtrade/freqai/data_drawer.py index dda8ebdbf..038ddaf2e 100644 --- a/freqtrade/freqai/data_drawer.py +++ b/freqtrade/freqai/data_drawer.py @@ -87,6 +87,7 @@ class FreqaiDataDrawer: self.create_follower_dict() self.load_drawer_from_disk() self.load_historic_predictions_from_disk() + self.metric_tracker: Dict[str, Dict[str, Dict[str, list]]] = {} self.load_metric_tracker_from_disk() self.training_queue: Dict[str, int] = {} self.history_lock = threading.Lock() @@ -97,7 +98,6 @@ class FreqaiDataDrawer: self.empty_pair_dict: pair_info = { "model_filename": "", "trained_timestamp": 0, "data_path": "", "extras": {}} - self.metric_tracker: Dict[str, Dict[str, Dict[str, list]]] = {} def update_metric_tracker(self, metric: str, value: float, pair: str) -> None: """ @@ -153,6 +153,7 @@ class FreqaiDataDrawer: if exists: with open(self.metric_tracker_path, "r") as fp: self.metric_tracker = rapidjson.load(fp, number_mode=rapidjson.NM_NATIVE) + logger.info("Loading existing metric tracker from disk.") else: logger.info("Could not find existing metric tracker, starting from scratch") From 66514e84e490250fe64e6c08329326f225ee59e3 Mon Sep 17 00:00:00 2001 From: robcaulk Date: Fri, 11 Nov 2022 17:45:53 +0100 Subject: [PATCH 11/11] add LightGBMClassifierMultiTarget. add test --- .../LightGBMClassifierMultiTarget.py | 64 +++++++++++++++++++ tests/freqai/test_freqai_interface.py | 3 +- 2 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 freqtrade/freqai/prediction_models/LightGBMClassifierMultiTarget.py diff --git a/freqtrade/freqai/prediction_models/LightGBMClassifierMultiTarget.py b/freqtrade/freqai/prediction_models/LightGBMClassifierMultiTarget.py new file mode 100644 index 000000000..d1eb6daa2 --- /dev/null +++ b/freqtrade/freqai/prediction_models/LightGBMClassifierMultiTarget.py @@ -0,0 +1,64 @@ +import logging +from typing import Any, Dict + +from lightgbm import LGBMClassifier + +from freqtrade.freqai.base_models.BaseClassifierModel import BaseClassifierModel +from freqtrade.freqai.base_models.FreqaiMultiOutputClassifier import FreqaiMultiOutputClassifier +from freqtrade.freqai.data_kitchen import FreqaiDataKitchen + + +logger = logging.getLogger(__name__) + + +class LightGBMClassifierMultiTarget(BaseClassifierModel): + """ + User created prediction model. The class needs to override three necessary + functions, predict(), train(), fit(). The class inherits ModelHandler which + has its own DataHandler where data is held, saved, loaded, and managed. + """ + + def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: + """ + User sets up the training and test data to fit their desired model here + :param data_dictionary: the dictionary constructed by DataHandler to hold + all the training and test data/labels. + """ + + lgb = LGBMClassifier(**self.model_training_parameters) + + X = data_dictionary["train_features"] + y = data_dictionary["train_labels"] + sample_weight = data_dictionary["train_weights"] + + eval_weights = None + eval_sets = [None] * y.shape[1] + + if self.freqai_info.get('data_split_parameters', {}).get('test_size', 0.1) != 0: + eval_weights = [data_dictionary["test_weights"]] + eval_sets = [(None, None)] * data_dictionary['test_labels'].shape[1] # type: ignore + for i in range(data_dictionary['test_labels'].shape[1]): + eval_sets[i] = ( # type: ignore + data_dictionary["test_features"], + data_dictionary["test_labels"].iloc[:, i] + ) + + init_model = self.get_init_model(dk.pair) + if init_model: + init_models = init_model.estimators_ + else: + init_models = [None] * y.shape[1] + + fit_params = [] + for i in range(len(eval_sets)): + fit_params.append( + {'eval_set': eval_sets[i], 'eval_sample_weight': eval_weights, + 'init_model': init_models[i]}) + + model = FreqaiMultiOutputClassifier(estimator=lgb) + thread_training = self.freqai_info.get('multitarget_parallel_training', False) + if thread_training: + model.n_jobs = y.shape[1] + model.fit(X=X, y=y, sample_weight=sample_weight, fit_params=fit_params) + + return model diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index 5b9453a4a..a49f7c882 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -79,8 +79,7 @@ def test_extract_data_and_train_model_Standard(mocker, freqai_conf, model): ('LightGBMRegressorMultiTarget', "freqai_test_multimodel_strat"), ('XGBoostRegressorMultiTarget', "freqai_test_multimodel_strat"), ('CatboostRegressorMultiTarget', "freqai_test_multimodel_strat"), - # ('LightGBMClassifierMultiTarget', "freqai_test_multimodel_classifier_strat"), - # ('XGBoostClassifierMultiTarget', "freqai_test_multimodel_classifier_strat"), + ('LightGBMClassifierMultiTarget', "freqai_test_multimodel_classifier_strat"), ('CatboostClassifierMultiTarget', "freqai_test_multimodel_classifier_strat") ]) def test_extract_data_and_train_model_MultiTargets(mocker, freqai_conf, model, strat):