import logging import numpy as np # noqa import pandas as pd # noqa import talib.abstract as ta from pandas import DataFrame from technical import qtpylib from freqtrade.strategy import IntParameter, IStrategy, merge_informative_pair logger = logging.getLogger(__name__) class FreqaiExampleHybridStrategy(IStrategy): """ Example of a hybrid FreqAI strat, designed to illustrate how a user may employ FreqAI to bolster a typical Freqtrade strategy. Launching this strategy would be: freqtrade trade --strategy FreqaiExampleHybridStrategy --strategy-path freqtrade/templates --freqaimodel CatboostClassifier --config config_examples/config_freqai.example.json or the user simply adds this to their config: "freqai": { "enabled": true, "purge_old_models": 2, "train_period_days": 15, "identifier": "unique-id", "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] }, "data_split_parameters": { "test_size": 0, "random_state": 1 }, "model_training_parameters": { "n_estimators": 200, "max_depth": 5, "learning_rate": 0.05 } }, Thanks to @smarmau and @johanvulgt for developing and sharing the strategy. """ minimal_roi = { # "120": 0.0, # exit after 120 minutes at break even "60": 0.01, "30": 0.02, "0": 0.04, } plot_config = { "main_plot": { "tema": {}, }, "subplots": { "MACD": { "macd": {"color": "blue"}, "macdsignal": {"color": "orange"}, }, "RSI": { "rsi": {"color": "red"}, }, "Up_or_down": { "&s-up_or_down": {"color": "green"}, }, }, } process_only_new_candles = True stoploss = -0.05 use_exit_signal = True startup_candle_count: int = 30 can_short = False # Hyperoptable parameters buy_rsi = IntParameter(low=1, high=50, default=30, space="buy", optimize=True, load=True) sell_rsi = IntParameter(low=50, high=100, default=70, space="sell", optimize=True, load=True) def feature_engineering_expand_all( self, dataframe: DataFrame, period: int, metadata: dict, **kwargs ) -> DataFrame: """ *Only functional with FreqAI enabled strategies* This function will automatically expand the defined features on the config defined `indicator_periods_candles`, `include_timeframes`, `include_shifted_candles`, and `include_corr_pairs`. In other words, a single feature defined in this function will automatically expand to a total of `indicator_periods_candles` * `include_timeframes` * `include_shifted_candles` * `include_corr_pairs` numbers of features added to the model. All features must be prepended with `%` to be recognized by FreqAI internals. More details on how these config defined parameters accelerate feature engineering in the documentation at: https://www.freqtrade.io/en/latest/freqai-parameter-table/#feature-parameters https://www.freqtrade.io/en/latest/freqai-feature-engineering/#defining-the-features :param dataframe: strategy dataframe which will receive the features :param period: period of the indicator - usage example: :param metadata: metadata of current pair dataframe["%-ema-period"] = ta.EMA(dataframe, timeperiod=period) """ dataframe["%-rsi-period"] = ta.RSI(dataframe, timeperiod=period) dataframe["%-ema-period"] = ta.EMA(dataframe, timeperiod=period) bollinger = qtpylib.bollinger_bands( qtpylib.typical_price(dataframe), window=period, stds=2 ) dataframe["bb_lowerband-period"] = bollinger["lower"] dataframe["bb_middleband-period"] = bollinger["mid"] dataframe["bb_upperband-period"] = bollinger["upper"] dataframe["%-bb_width-period"] = ( dataframe["bb_upperband-period"] - dataframe["bb_lowerband-period"] ) / dataframe["bb_middleband-period"] return dataframe def feature_engineering_expand_basic( self, dataframe: DataFrame, metadata: dict, **kwargs ) -> DataFrame: """ *Only functional with FreqAI enabled strategies* This function will automatically expand the defined features on the config defined `include_timeframes`, `include_shifted_candles`, and `include_corr_pairs`. In other words, a single feature defined in this function will automatically expand to a total of `include_timeframes` * `include_shifted_candles` * `include_corr_pairs` numbers of features added to the model. Features defined here will *not* be automatically duplicated on user defined `indicator_periods_candles` All features must be prepended with `%` to be recognized by FreqAI internals. More details on how these config defined parameters accelerate feature engineering in the documentation at: https://www.freqtrade.io/en/latest/freqai-parameter-table/#feature-parameters https://www.freqtrade.io/en/latest/freqai-feature-engineering/#defining-the-features :param dataframe: strategy dataframe which will receive the features :param metadata: metadata of current pair dataframe["%-pct-change"] = dataframe["close"].pct_change() dataframe["%-ema-200"] = ta.EMA(dataframe, timeperiod=200) """ dataframe["%-pct-change"] = dataframe["close"].pct_change() return dataframe def feature_engineering_standard( self, dataframe: DataFrame, metadata: dict, **kwargs ) -> DataFrame: """ *Only functional with FreqAI enabled strategies* This optional function will be called once with the dataframe of the base timeframe. This is the final function to be called, which means that the dataframe entering this function will contain all the features and columns created by all other freqai_feature_engineering_* functions. This function is a good place to do custom exotic feature extractions (e.g. tsfresh). This function is a good place for any feature that should not be auto-expanded upon (e.g. day of the week). All features must be prepended with `%` to be recognized by FreqAI internals. More details about feature engineering available: https://www.freqtrade.io/en/latest/freqai-feature-engineering :param dataframe: strategy dataframe which will receive the features :param metadata: metadata of current pair usage example: dataframe["%-day_of_week"] = (dataframe["date"].dt.dayofweek + 1) / 7 """ dataframe["%-day_of_week"] = dataframe["date"].dt.dayofweek dataframe["%-hour_of_day"] = dataframe["date"].dt.hour return dataframe def set_freqai_targets(self, dataframe: DataFrame, metadata: dict, **kwargs) -> DataFrame: """ Redefined target variable to predict whether the price will increase or decrease in the future. """ logger.info(f"Setting FreqAI targets for pair: {metadata['pair']}") if "close" not in dataframe.columns: logger.error("Required 'close' column missing in dataframe") raise ValueError("Required 'close' column missing in dataframe") if len(dataframe) < 50: logger.error(f"Insufficient data: {len(dataframe)} rows, need at least 50 for shift(-50)") raise ValueError("Insufficient data for target calculation") try: # Define target variable: 1 for price increase, 0 for price decrease dataframe["&-up_or_down"] = np.where( dataframe["close"].shift(-50) > dataframe["close"], 1, 0 ) # Ensure target variable is a 2D array dataframe["&-up_or_down"] = dataframe["&-up_or_down"].values.reshape(-1, 1) except Exception as e: logger.error(f"Failed to create &-up_or_down column: {str(e)}") raise logger.info("FreqAI targets set successfully") return dataframe def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: logger.info(f"Processing pair: {metadata['pair']}") logger.info(f"Input DataFrame shape: {dataframe.shape}") logger.info(f"Input DataFrame columns: {list(dataframe.columns)}") logger.info(f"Input DataFrame head:\n{dataframe[['date', 'close', 'volume']].head().to_string()}") # Ensure FreqAI processing logger.info("Calling self.freqai.start") try: dataframe = self.freqai.start(dataframe, metadata, self) except Exception as e: logger.error(f"self.freqai.start failed: {str(e)}") raise logger.info("self.freqai.start completed") logger.info(f"Output DataFrame shape: {dataframe.shape}") logger.info(f"Output DataFrame columns: {list(dataframe.columns)}") # Safely log columns that exist available_columns = [col for col in ['date', 'close', '&-up_or_down'] if col in dataframe.columns] logger.info(f"Output DataFrame head:\n{dataframe[available_columns].head().to_string()}") if "&-up_or_down" not in dataframe.columns: logger.error("FreqAI did not generate the required &-up_or_down column") raise KeyError("FreqAI did not generate the required &-up_or_down column") # RSI dataframe["rsi"] = ta.RSI(dataframe) # Bollinger Bands bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2) dataframe["bb_lowerband"] = bollinger["lower"] dataframe["bb_middleband"] = bollinger["mid"] dataframe["bb_upperband"] = bollinger["upper"] dataframe["bb_percent"] = (dataframe["close"] - dataframe["bb_lowerband"]) / ( dataframe["bb_upperband"] - dataframe["bb_lowerband"] ) dataframe["bb_width"] = (dataframe["bb_upperband"] - dataframe["bb_lowerband"]) / dataframe[ "bb_middleband" ] # TEMA dataframe["tema"] = ta.TEMA(dataframe, timeperiod=9) return dataframe def populate_entry_trend(self, df: DataFrame, metadata: dict) -> DataFrame: df.loc[ ( (qtpylib.crossed_above(df["rsi"], self.buy_rsi.value)) & (df["tema"] <= df["bb_middleband"]) & (df["tema"] > df["tema"].shift(1)) & (df["volume"] > 0) ), "enter_long", ] = 1 return df def populate_exit_trend(self, df: DataFrame, metadata: dict) -> DataFrame: df.loc[ ( (qtpylib.crossed_above(df["rsi"], self.sell_rsi.value)) & (df["tema"] > df["bb_middleband"]) & (df["tema"] < df["tema"].shift(1)) & (df["volume"] > 0) ), "exit_long", ] = 1 return df