From 47056eded3429a328b8b92d339738fca60e73818 Mon Sep 17 00:00:00 2001 From: Mark Regan Date: Tue, 25 Oct 2022 18:24:27 +0100 Subject: [PATCH 01/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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 2068a44fd014d8cba2c19bb24ea4738b635fdf44 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 6 Nov 2022 11:16:19 +0100 Subject: [PATCH 10/24] Add test for new behavior --- tests/test_worker.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/test_worker.py b/tests/test_worker.py index ae511852f..88d495e13 100644 --- a/tests/test_worker.py +++ b/tests/test_worker.py @@ -113,6 +113,16 @@ def test_throttle_sleep_time(mocker, default_conf, caplog) -> None: # 300 (5m) - 60 (1m - see set time above) - 5 (duration of throttled_func) = 235 assert 235.2 < sleep_mock.call_args[0][0] < 235.6 + t.move_to("2022-09-01 05:04:51 +00:00") + sleep_mock.reset_mock() + # Offset of 5s, so we hit the sweet-spot between "candle" and "candle offset" + # Which should not get a throttle iteration to avoid late candle fetching + assert worker._throttle(throttled_func, throttle_secs=10, timeframe='5m', + timeframe_offset=5, x=1.2) == 42 + assert sleep_mock.call_count == 1 + # Time is slightly bigger than throttle secs due to the high timeframe offset. + assert 11.1 < sleep_mock.call_args[0][0] < 13.2 + def test_throttle_with_assets(mocker, default_conf) -> None: def throttled_func(nb_assets=-1): From 29585b5ecd07159d1993bf4f2fa3df356fbf3bc4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 6 Nov 2022 11:18:13 +0100 Subject: [PATCH 11/24] Improve worker iteration logic --- freqtrade/worker.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/freqtrade/worker.py b/freqtrade/worker.py index a407de0d7..27f067b07 100755 --- a/freqtrade/worker.py +++ b/freqtrade/worker.py @@ -150,14 +150,20 @@ class Worker: if timeframe: next_tf = timeframe_to_next_date(timeframe) # Maximum throttling should be until new candle arrives - # Offset of 0.2s is added to ensure a new candle has been issued. - next_tf_with_offset = next_tf.timestamp() - time.time() + timeframe_offset + # Offset is added to ensure a new candle has been issued. + next_tft = next_tf.timestamp() - time.time() + next_tf_with_offset = next_tft + timeframe_offset + if next_tft < sleep_duration and sleep_duration < next_tf_with_offset: + # Avoid hitting a new loop between the new candle and the candle with offset + sleep_duration = next_tf_with_offset sleep_duration = min(sleep_duration, next_tf_with_offset) sleep_duration = max(sleep_duration, 0.0) # next_iter = datetime.now(timezone.utc) + timedelta(seconds=sleep_duration) logger.debug(f"Throttling with '{func.__name__}()': sleep for {sleep_duration:.2f} s, " - f"last iteration took {time_passed:.2f} s.") + f"last iteration took {time_passed:.2f} s." + # f"next: {next_iter}" + ) self._sleep(sleep_duration) return result From f43f967040a18ac09643bb7f1c562e499a4143d5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 8 Nov 2022 20:34:18 +0100 Subject: [PATCH 12/24] Improve handling of unfilled stoploss orders in edge-cases --- freqtrade/persistence/trade_model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 70c460e89..743aa5eba 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -667,7 +667,7 @@ class LocalTrade(): self.close(order.safe_price) else: self.recalc_trade_from_orders() - elif order.ft_order_side == 'stoploss': + elif order.ft_order_side == 'stoploss' and order.status not in ('canceled', 'open'): self.stoploss_order_id = None self.close_rate_requested = self.stop_loss self.exit_reason = ExitType.STOPLOSS_ON_EXCHANGE.value From d3006f7f3e7f5e502bb8331f2b85bef089cff995 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 9 Nov 2022 17:59:51 +0100 Subject: [PATCH 13/24] Bump ccxt to 2.1.54 closes okx: #7720 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 0363a4740..b98973cc7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ numpy==1.23.4 pandas==1.5.1 pandas-ta==0.3.14b -ccxt==2.1.33 +ccxt==2.1.54 # Pin cryptography for now due to rust build errors with piwheels cryptography==38.0.1 aiohttp==3.8.3 From 037363f9ee3f090acd12284da268cf3489815c9d Mon Sep 17 00:00:00 2001 From: Tim Date: Wed, 9 Nov 2022 18:51:25 +0000 Subject: [PATCH 14/24] support git and local changes in dev containers #7723 --- .devcontainer/devcontainer.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 41b8475ec..786d317a6 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -11,12 +11,13 @@ "mounts": [ "source=freqtrade-bashhistory,target=/home/ftuser/commandhistory,type=volume" ], + "workspaceMount": "source=${localWorkspaceFolder},target=/workspaces/freqtrade,type=bind,consistency=cached", // Uncomment to connect as a non-root user if you've added one. See https://aka.ms/vscode-remote/containers/non-root. "remoteUser": "ftuser", - "postCreateCommand": "freqtrade create-userdir --userdir user_data/", + "postCreateCommand": "ln -s /freqtrade/.env /workspaces/freqtrade/.env && freqtrade create-userdir --userdir user_data/", - "workspaceFolder": "/freqtrade/", + "workspaceFolder": "/workspaces/freqtrade", "settings": { "terminal.integrated.shell.linux": "/bin/bash", @@ -35,5 +36,6 @@ "davidanson.vscode-markdownlint", "ms-azuretools.vscode-docker", "vscode-icons-team.vscode-icons", + "github.copilot", ], } From 795328051346d23dc3390f434c089c4dd13ccfbf Mon Sep 17 00:00:00 2001 From: Tim Date: Wed, 9 Nov 2022 21:05:05 +0100 Subject: [PATCH 15/24] remove github.copilot extension --- .devcontainer/devcontainer.json | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 786d317a6..03adeacff 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -15,9 +15,7 @@ // Uncomment to connect as a non-root user if you've added one. See https://aka.ms/vscode-remote/containers/non-root. "remoteUser": "ftuser", - "postCreateCommand": "ln -s /freqtrade/.env /workspaces/freqtrade/.env && freqtrade create-userdir --userdir user_data/", - - "workspaceFolder": "/workspaces/freqtrade", + "postCreateCommand": "ln -s /freqtrade/.env /workspaces/freqtrade/.env && freqtrade create-userdir --userdir user_data/", "settings": { "terminal.integrated.shell.linux": "/bin/bash", @@ -35,7 +33,6 @@ "ms-python.vscode-pylance", "davidanson.vscode-markdownlint", "ms-azuretools.vscode-docker", - "vscode-icons-team.vscode-icons", - "github.copilot", + "vscode-icons-team.vscode-icons", ], } From ec6ee7ead9fb3dc62d86eba4b7b3e28a4ff67a01 Mon Sep 17 00:00:00 2001 From: Tim Date: Wed, 9 Nov 2022 21:06:14 +0100 Subject: [PATCH 16/24] remove empty space --- .devcontainer/devcontainer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 03adeacff..18a818430 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -15,7 +15,7 @@ // Uncomment to connect as a non-root user if you've added one. See https://aka.ms/vscode-remote/containers/non-root. "remoteUser": "ftuser", - "postCreateCommand": "ln -s /freqtrade/.env /workspaces/freqtrade/.env && freqtrade create-userdir --userdir user_data/", + "postCreateCommand": "ln -s /freqtrade/.env /workspaces/freqtrade/.env && freqtrade create-userdir --userdir user_data/", "settings": { "terminal.integrated.shell.linux": "/bin/bash", @@ -33,6 +33,6 @@ "ms-python.vscode-pylance", "davidanson.vscode-markdownlint", "ms-azuretools.vscode-docker", - "vscode-icons-team.vscode-icons", + "vscode-icons-team.vscode-icons", ], } From 9e17eabd0a615641af582461ae8cb0b73e654bcb Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 10 Nov 2022 07:09:54 +0100 Subject: [PATCH 17/24] Improve Bybit configuration --- freqtrade/exchange/bybit.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/bybit.py b/freqtrade/exchange/bybit.py index 1c4bb858b..641540c89 100644 --- a/freqtrade/exchange/bybit.py +++ b/freqtrade/exchange/bybit.py @@ -21,7 +21,11 @@ class Bybit(Exchange): _ft_has: Dict = { "ohlcv_candle_limit": 200, - "ccxt_futures_name": "linear" + "ccxt_futures_name": "linear", + "ohlcv_has_history": False, + } + _ft_has_futures: Dict = { + "ohlcv_has_history": True, } _supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [ From 22c419d5c46522db9006c0d1023e4203e9ff47ba Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 10 Nov 2022 07:14:02 +0100 Subject: [PATCH 18/24] Add warning about FTX --- docs/exchanges.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/exchanges.md b/docs/exchanges.md index 980d102b2..bae7c929c 100644 --- a/docs/exchanges.md +++ b/docs/exchanges.md @@ -175,6 +175,10 @@ print(res) ## FTX +!!! Warning + Due to the current situation, we can no longer recommend FTX. + Please make sure to investigate the current situation before depositing any funds to FTX. + !!! Tip "Stoploss on Exchange" FTX supports `stoploss_on_exchange` and can use both stop-loss-market and stop-loss-limit orders. It provides great advantages, so we recommend to benefit from it. You can use either `"limit"` or `"market"` in the `order_types.stoploss` configuration setting to decide which type of stoploss shall be used. From 88ad3fe43e71542bbf2db22a5b0e053b6ad1e9a4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 10 Nov 2022 07:32:40 +0100 Subject: [PATCH 19/24] Remove typo from main page --- README.md | 2 +- docs/index.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 0cc2364e5..c0452fa85 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ Please read the [exchange specific notes](docs/exchanges.md) to learn about even - [X] [Binance](https://www.binance.com/) - [X] [Gate.io](https://www.gate.io/ref/6266643) -- [X] [OKX](https://okx.com/). +- [X] [OKX](https://okx.com/) Please make sure to read the [exchange specific notes](docs/exchanges.md), as well as the [trading with leverage](docs/leverage.md) documentation before diving in. diff --git a/docs/index.md b/docs/index.md index 7c35e92b6..5b4add52c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -32,7 +32,7 @@ Freqtrade is a free and open source crypto trading bot written in Python. It is - Run: Test your strategy with simulated money (Dry-Run mode) or deploy it with real money (Live-Trade mode). - Run using Edge (optional module): The concept is to find the best historical [trade expectancy](edge.md#expectancy) by markets based on variation of the stop-loss and then allow/reject markets to trade. The sizing of the trade is based on a risk of a percentage of your capital. - Control/Monitor: Use Telegram or a WebUI (start/stop the bot, show profit/loss, daily summary, current open trades results, etc.). -- Analyse: Further analysis can be performed on either Backtesting data or Freqtrade trading history (SQL database), including automated standard plots, and methods to load the data into [interactive environments](data-analysis.md). +- Analyze: Further analysis can be performed on either Backtesting data or Freqtrade trading history (SQL database), including automated standard plots, and methods to load the data into [interactive environments](data-analysis.md). ## Supported exchange marketplaces @@ -51,7 +51,7 @@ Please read the [exchange specific notes](exchanges.md) to learn about eventual, - [X] [Binance](https://www.binance.com/) - [X] [Gate.io](https://www.gate.io/ref/6266643) -- [X] [OKX](https://okx.com/). +- [X] [OKX](https://okx.com/) Please make sure to read the [exchange specific notes](exchanges.md), as well as the [trading with leverage](leverage.md) documentation before diving in. From be83e73411f08605682e0d614ca9266e6fd7f323 Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 10 Nov 2022 08:42:47 +0000 Subject: [PATCH 20/24] add pip install --- .devcontainer/devcontainer.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 18a818430..298ac07cc 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -15,7 +15,9 @@ // Uncomment to connect as a non-root user if you've added one. See https://aka.ms/vscode-remote/containers/non-root. "remoteUser": "ftuser", - "postCreateCommand": "ln -s /freqtrade/.env /workspaces/freqtrade/.env && freqtrade create-userdir --userdir user_data/", + "postCreateCommand": "pip install --user -e . && freqtrade create-userdir --userdir user_data/", + + "workspaceFolder": "/workspaces/freqtrade", "settings": { "terminal.integrated.shell.linux": "/bin/bash", From 7147f52e025b3e09d33109a80a02d849e293e52f Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 10 Nov 2022 16:03:30 +0100 Subject: [PATCH 21/24] FreqAI also requires plotting dependencies cloess #7726 --- requirements-freqai.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements-freqai.txt b/requirements-freqai.txt index a298ad489..a90b9df69 100644 --- a/requirements-freqai.txt +++ b/requirements-freqai.txt @@ -1,5 +1,6 @@ # Include all requirements to run the bot. -r requirements.txt +-r requirements-plot.txt # Required for freqai scikit-learn==1.1.3 From 4664d5e1d8615879653e8e1328070d8735589f19 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 10 Nov 2022 18:56:19 +0000 Subject: [PATCH 22/24] Split installation to onCreateCommand --- .devcontainer/devcontainer.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 298ac07cc..8fb643e8f 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -15,7 +15,8 @@ // Uncomment to connect as a non-root user if you've added one. See https://aka.ms/vscode-remote/containers/non-root. "remoteUser": "ftuser", - "postCreateCommand": "pip install --user -e . && freqtrade create-userdir --userdir user_data/", + "onCreateCommand": "pip install --user -e .", + "postCreateCommand": "freqtrade create-userdir --userdir user_data/", "workspaceFolder": "/workspaces/freqtrade", From 054133955b4c67312d053519308b43ed7a680788 Mon Sep 17 00:00:00 2001 From: robcaulk Date: Fri, 11 Nov 2022 17:24:09 +0100 Subject: [PATCH 23/24] 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 24/24] 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):