import pandas as pd import numpy as np # import talib # Removed TA-Lib dependency from freqtrade.strategy import IStrategy, IntParameter, DecimalParameter, CategoricalParameter import joblib # For loading the model import os # Define thresholds for target variable calculation (used during OFFLINE training) # These are hyperparameters for the problem definition TARGET_THRESHOLD_PCT = 0.005 # e.g., 0.5% price movement LOOKAHEAD_PERIODS = 4 # e.g., predict for 4 candles ahead class MLXGBoostStrategy(IStrategy): """ Trading strategy using a pre-trained XGBoost classifier. Features are calculated using standard technical indicators. The target variable (Buy/Sell/Hold based on future price movement) is calculated OFFLINE during model training. This strategy LOADS a pre-trained model and uses it for predictions. """ # --- Strategy Hyperparameters --- # Define model path as a parameter for flexibility model_path = CategoricalParameter( ['user_data/models/xgb_model.joblib', '/path/to/your/model.joblib'], default='user_data/models/xgb_model.joblib', space='buy' ) # Add other strategy hyperparameters from EnhancedMAStrategy if needed for reference # (These are NOT used by the ML model directly but might be useful context or for hybrid approaches) # --- Removed TA-Lib dependent parameters --- # fast_ma_period = IntParameter(5, 20, default=9, space='buy') # medium_ma_period = IntParameter(15, 30, default=21, space='buy') # slow_ma_period = IntParameter(40, 60, default=50, space='buy') # adx_period = IntParameter(10, 20, default=14, space='buy') # adx_threshold = IntParameter(20, 35, default=25, space='buy') # volume_ma_period = IntParameter(15, 30, default=20, space='buy') # Keeping volume MA - not TA-Lib # volume_factor = DecimalParameter(1.0, 2.5, default=1.5, space='buy') # Keeping volume factor # atr_period = IntParameter(10, 20, default=14, space='buy') # rsi_period = IntParameter(10, 20, default=14, space='buy') # --- Minimal ROI and Stoploss (Required by Freqtrade) --- minimal_roi = { "0": 0.10, # Example: 10% profit after 0 minutes (uses exit signal mainly) "60": 0.05, # Example: 5% after 1 hour "120": 0.02 # Example: 2% after 2 hours } stoploss = -0.10 # Example: 10% stoploss # Timeframe needs to be defined timeframe = '1h' # Example timeframe - should match data used for training # --- Model Loading --- _model = None @property def model(self): """Load the model lazily.""" if self._model is None: model_file = self.model_path.value if os.path.exists(model_file): try: self._model = joblib.load(model_file) print(f"Successfully loaded model from {model_file}") except Exception as e: print(f"Error loading model from {model_file}: {e}") # Decide how to handle failure: stop bot, fall back, etc. # For now, we'll let it fail later during prediction else: print(f"Model file not found at {model_file}. Prediction will fail.") return self._model # --- Feature Engineering (Indicators) --- def populate_indicators(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame: """ Calculate all technical indicators that will be used as features for the ML model. REMOVED TA-LIB INDICATORS. """ # --- Basic OHLCV features (Example - Expand this!) --- dataframe['price_change_pct'] = dataframe['close'].pct_change() dataframe['high_low_diff'] = dataframe['high'] - dataframe['low'] dataframe['close_open_diff'] = dataframe['close'] - dataframe['open'] # --- Volume features (Not TA-Lib based) --- dataframe['volume_ma_20'] = dataframe['volume'].rolling(window=20).mean() dataframe['volume_ratio'] = np.where( dataframe['volume_ma_20'] > 0, dataframe['volume'] / dataframe['volume_ma_20'], 1 ) # ---- Add more NON-TA-LIB features here! Examples: ---- # Rolling window features dataframe['rolling_mean_close_5'] = dataframe['close'].rolling(window=5).mean() dataframe['rolling_std_close_20'] = dataframe['close'].rolling(window=20).std() # Time-based features dataframe['hour_of_day'] = dataframe.index.hour dataframe['day_of_week'] = dataframe.index.dayofweek return dataframe # --- Prediction Logic (Buy/Sell Signals) --- def populate_buy_trend(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame: """ Use the loaded ML model to predict the buy signal. Target classes (example): 0=Hold, 1=Buy, 2=Sell """ dataframe['buy'] = 0 # Default to no buy signal if self.model is None: print("Model not loaded, cannot generate buy signals.") return dataframe # Prepare features for the model (ensure order and scaling matches training) # This needs to be robust and match the *exact* preprocessing used during training # UPDATE THIS LIST TO MATCH populate_indicators (NO TA-LIB) feature_columns = [ 'price_change_pct', 'high_low_diff', 'close_open_diff', 'volume_ma_20', 'volume_ratio', 'rolling_mean_close_5', 'rolling_std_close_20', 'hour_of_day', 'day_of_week' # Add all features used in training ] # Check if all required columns exist and handle potential NaNs if not all(col in dataframe.columns for col in feature_columns): print(f"Missing required feature columns for prediction: {dataframe.columns.tolist()} vs {feature_columns}") return dataframe # Select only the features the model was trained on features = dataframe[feature_columns] # Handle any remaining NaNs (e.g., fill with 0 or a mean, MUST match training) features = features.fillna(0) # Example: Fill NaNs with 0 - ADJUST AS NEEDED # Predict try: predictions = self.model.predict(features) # Use predict_proba for probabilities if needed # Assuming target 1 means "Buy" dataframe.loc[predictions == 1, 'buy'] = 1 except Exception as e: print(f"Error during model prediction (buy): {e}") # Potentially add error handling or fallback logic # print(f"Buy signals generated: {dataframe['buy'].sum()}") # Basic logging return dataframe def populate_sell_trend(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame: """ Use the loaded ML model to predict the sell signal. Target classes (example): 0=Hold, 1=Buy, 2=Sell """ dataframe['sell'] = 0 # Default to no sell signal if self.model is None: print("Model not loaded, cannot generate sell signals.") return dataframe # Prepare features (same process as in populate_buy_trend) # UPDATE THIS LIST TO MATCH populate_indicators (NO TA-LIB) feature_columns = [ 'price_change_pct', 'high_low_diff', 'close_open_diff', 'volume_ma_20', 'volume_ratio', 'rolling_mean_close_5', 'rolling_std_close_20', 'hour_of_day', 'day_of_week' # Add all features used in training ] if not all(col in dataframe.columns for col in feature_columns): print(f"Missing required feature columns for prediction: {dataframe.columns.tolist()} vs {feature_columns}") return dataframe features = dataframe[feature_columns].fillna(0) # MUST match training # Predict try: predictions = self.model.predict(features) # Assuming target 2 means "Sell" dataframe.loc[predictions == 2, 'sell'] = 1 except Exception as e: print(f"Error during model prediction (sell): {e}") # print(f"Sell signals generated: {dataframe['sell'].sum()}") # Basic logging return dataframe # --- Optional: Custom Exit Logic (Can be ML-based too) --- # def custom_exit(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float, # current_profit: float, **kwargs): # """ # Optional: Implement custom exit logic, potentially using another ML model # or specific conditions based on the entry signal model's prediction confidence. # """ # # Example: Exit if prediction confidence drops below a threshold # return None # Return string reason to exit, or None to keep trade