# source: https://raw.githubusercontent.com/smpsandro1239/Freqtrade/82e4757c0650b6ad6fa419d9b006ce6679bc8d1e/user_data/strategies/MLStrategySimple.py
# -*- coding: utf-8 -*-
"""
Github_smpsandro1239_Freqtrade__MLStrategySimple__20250812_174418 - Machine Learning Based Trading Strategy (Simplified)
Usa implementações nativas de indicadores técnicos + scikit-learn
"""
import logging
import os
import pickle
from datetime import datetime
from pathlib import Path

import numpy as np
import pandas as pd

from freqtrade.strategy import (CategoricalParameter, DecimalParameter,
                                IntParameter, IStrategy)

# ML imports
try:
    from sklearn.ensemble import RandomForestClassifier
    from sklearn.metrics import accuracy_score
    from sklearn.model_selection import train_test_split
    from sklearn.preprocessing import StandardScaler
    ML_AVAILABLE = True
except ImportError:
    ML_AVAILABLE = False
    logging.warning("⚠️ scikit-learn não disponível. Usando fallback para indicadores técnicos.")

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

class Github_smpsandro1239_Freqtrade__MLStrategySimple__20250812_174418(IStrategy):
    """
    Estratégia baseada em Machine Learning com indicadores nativos

    Features utilizadas:
    - RSI, EMA, SMA
    - Volume patterns
    - Price momentum
    - Volatility measures
    """

    INTERFACE_VERSION = 3

    # Configurações básicas
    timeframe = '5m'
    stoploss = -0.08
    minimal_roi = {
        "0": 0.05,
        "10": 0.03,
        "20": 0.02,
        "30": 0.01,
        "60": 0.005
    }

    # Trailing stop
    trailing_stop = True
    trailing_stop_positive = 0.02
    trailing_stop_positive_offset = 0.03
    trailing_only_offset_is_reached = True

    # Parâmetros otimizáveis
    ml_lookback = IntParameter(50, 200, default=100, space='buy')
    ml_threshold = DecimalParameter(0.6, 0.9, default=0.75, space='buy')
    retrain_interval = IntParameter(100, 500, default=200, space='buy')

    # Proteções
    cooldown_lookback = IntParameter(2, 48, default=10, space="protection")
    stop_duration = IntParameter(12, 200, default=20, space="protection")
    use_stop_protection = CategoricalParameter([True, False], default=True, space="protection")

    def __init__(self, config: dict) -> None:
        super().__init__(config)
        self.model = None
        self.scaler = StandardScaler() if ML_AVAILABLE else None
        self.model_path = Path("user_data/ml_models")
        self.model_path.mkdir(exist_ok=True)
        self.last_train_candle = 0
        self.feature_columns = []

        if not ML_AVAILABLE:
            logger.warning("🚨 ML não disponível - usando estratégia técnica como fallback")

    def get_int_value(self, param):
        """Helper para converter parâmetros para int"""
        return int(param.value)

    @property
    def protections(self):
        if not self.use_stop_protection.value:
            return []
        return [
            {
                "method": "CooldownPeriod",
                "stop_duration_candles": self.get_int_value(self.stop_duration)
            },
            {
                "method": "StoplossGuard",
                "lookback_period_candles": self.get_int_value(self.cooldown_lookback),
                "trade_limit": 3,
                "stop_duration_candles": self.get_int_value(self.stop_duration),
                "only_per_pair": False
            }
        ]

    def rsi(self, series: pd.Series, period: int = 14) -> pd.Series:
        """Implementação nativa do RSI"""
        delta = series.diff()
        gain = (delta.where(delta > 0, 0)).rolling(window=period).mean()
        loss = (-delta.where(delta < 0, 0)).rolling(window=period).mean()
        rs = gain / loss
        return 100 - (100 / (1 + rs))

    def ema(self, series: pd.Series, period: int) -> pd.Series:
        """Implementação nativa da EMA"""
        return series.ewm(span=period).mean()

    def sma(self, series: pd.Series, period: int) -> pd.Series:
        """Implementação nativa da SMA"""
        return series.rolling(window=period).mean()

    def macd(self, series: pd.Series, fast: int = 12, slow: int = 26, signal: int = 9):
        """Implementação nativa do MACD"""
        ema_fast = self.ema(series, fast)
        ema_slow = self.ema(series, slow)
        macd_line = ema_fast - ema_slow
        signal_line = self.ema(macd_line, signal)
        histogram = macd_line - signal_line
        return macd_line, signal_line, histogram

    def bollinger_bands(self, series: pd.Series, period: int = 20, std_dev: int = 2):
        """Implementação nativa das Bollinger Bands"""
        sma = self.sma(series, period)
        std = series.rolling(window=period).std()
        upper = sma + (std * std_dev)
        lower = sma - (std * std_dev)
        return upper, sma, lower

    def atr(self, high: pd.Series, low: pd.Series, close: pd.Series, period: int = 14):
        """Implementação nativa do ATR"""
        tr1 = high - low
        tr2 = abs(high - close.shift())
        tr3 = abs(low - close.shift())
        tr = pd.concat([tr1, tr2, tr3], axis=1).max(axis=1)
        return tr.rolling(window=period).mean()

    def create_features(self, dataframe: pd.DataFrame) -> pd.DataFrame:
        """
        Criar features para o modelo ML usando implementações nativas
        """
        # RSI
        dataframe['rsi'] = self.rsi(dataframe['close'], 14)
        dataframe['rsi_fast'] = self.rsi(dataframe['close'], 7)
        dataframe['rsi_slow'] = self.rsi(dataframe['close'], 21)

        # MACD
        macd_line, signal_line, histogram = self.macd(dataframe['close'])
        dataframe['macd'] = macd_line
        dataframe['macd_signal'] = signal_line
        dataframe['macd_hist'] = histogram

        # Bollinger Bands
        bb_upper, bb_middle, bb_lower = self.bollinger_bands(dataframe['close'])
        dataframe['bb_upper'] = bb_upper
        dataframe['bb_middle'] = bb_middle
        dataframe['bb_lower'] = bb_lower
        dataframe['bb_percent'] = (dataframe['close'] - bb_lower) / (bb_upper - bb_lower)
        dataframe['bb_width'] = (bb_upper - bb_lower) / bb_middle

        # EMAs
        dataframe['ema_8'] = self.ema(dataframe['close'], 8)
        dataframe['ema_21'] = self.ema(dataframe['close'], 21)
        dataframe['ema_50'] = self.ema(dataframe['close'], 50)

        # SMAs
        dataframe['sma_20'] = self.sma(dataframe['close'], 20)
        dataframe['sma_50'] = self.sma(dataframe['close'], 50)

        # Price momentum
        dataframe['price_change_1'] = dataframe['close'].pct_change(1)
        dataframe['price_change_5'] = dataframe['close'].pct_change(5)
        dataframe['price_change_10'] = dataframe['close'].pct_change(10)

        # Volume features
        dataframe['volume_sma'] = self.sma(dataframe['volume'], 20)
        dataframe['volume_ratio'] = dataframe['volume'] / dataframe['volume_sma']
        dataframe['volume_change'] = dataframe['volume'].pct_change(1)

        # Volatility
        dataframe['atr'] = self.atr(dataframe['high'], dataframe['low'], dataframe['close'], 14)
        dataframe['volatility'] = dataframe['close'].rolling(20).std()

        # Support/Resistance levels
        dataframe['high_20'] = dataframe['high'].rolling(20).max()
        dataframe['low_20'] = dataframe['low'].rolling(20).min()
        dataframe['close_to_high'] = (dataframe['close'] - dataframe['low_20']) / (dataframe['high_20'] - dataframe['low_20'])

        # Trend indicators
        dataframe['ema_trend'] = (dataframe['ema_8'] > dataframe['ema_21']).astype(int)
        dataframe['sma_trend'] = (dataframe['close'] > dataframe['sma_20']).astype(int)

        return dataframe

    def create_target(self, dataframe: pd.DataFrame, lookahead: int = 5) -> pd.Series:
        """
        Criar target para treinamento (1 = buy signal, 0 = no signal)
        """
        # Target: preço sobe mais de 1% nos próximos 5 candles
        future_max = dataframe['high'].rolling(window=lookahead, min_periods=1).max().shift(-lookahead)
        target = (future_max / dataframe['close'] - 1) > 0.01
        return target.astype(int)

    def prepare_ml_data(self, dataframe: pd.DataFrame):
        """
        Preparar dados para ML
        """
        # Features para ML
        feature_cols = [
            'rsi', 'rsi_fast', 'rsi_slow',
            'macd', 'macd_signal', 'macd_hist',
            'bb_percent', 'bb_width',
            'price_change_1', 'price_change_5', 'price_change_10',
            'volume_ratio', 'volume_change',
            'atr', 'volatility',
            'close_to_high', 'ema_trend', 'sma_trend'
        ]

        # Filtrar apenas colunas que existem
        available_cols = [col for col in feature_cols if col in dataframe.columns]
        self.feature_columns = available_cols

        # Preparar features
        X = dataframe[available_cols].copy()

        # Remover NaN
        X = X.fillna(method='ffill').fillna(0)

        return X

    def train_model(self, dataframe: pd.DataFrame):
        """
        Treinar modelo ML
        """
        if not ML_AVAILABLE:
            return False

        try:
            logger.info("🤖 Treinando modelo ML...")

            # Preparar dados
            X = self.prepare_ml_data(dataframe)
            y = self.create_target(dataframe)

            # Remover últimas linhas (sem target)
            valid_idx = ~y.isna()
            X = X[valid_idx]
            y = y[valid_idx]

            if len(X) < 100:
                logger.warning("⚠️ Dados insuficientes para treinamento ML")
                return False

            # Split train/test
            X_train, X_test, y_train, y_test = train_test_split(
                X, y, test_size=0.2, random_state=42, stratify=y
            )

            # Normalizar features
            X_train_scaled = self.scaler.fit_transform(X_train)
            X_test_scaled = self.scaler.transform(X_test)

            # Treinar modelo
            self.model = RandomForestClassifier(
                n_estimators=100,
                max_depth=10,
                min_samples_split=5,
                min_samples_leaf=2,
                random_state=42,
                n_jobs=-1
            )

            self.model.fit(X_train_scaled, y_train)

            # Avaliar modelo
            y_pred = self.model.predict(X_test_scaled)
            accuracy = accuracy_score(y_test, y_pred)

            logger.info(f"✅ Modelo treinado - Acurácia: {accuracy:.3f}")

            # Salvar modelo
            model_file = self.model_path / "ml_model_simple.pkl"
            with open(model_file, 'wb') as f:
                pickle.dump({
                    'model': self.model,
                    'scaler': self.scaler,
                    'features': self.feature_columns,
                    'accuracy': accuracy
                }, f)

            return True

        except Exception as e:
            logger.error(f"❌ Erro no treinamento ML: {e}")
            return False

    def load_model(self):
        """
        Carregar modelo salvo
        """
        model_file = self.model_path / "ml_model_simple.pkl"
        if model_file.exists():
            try:
                with open(model_file, 'rb') as f:
                    data = pickle.load(f)
                    self.model = data['model']
                    self.scaler = data['scaler']
                    self.feature_columns = data['features']
                logger.info("✅ Modelo ML carregado")
                return True
            except Exception as e:
                logger.error(f"❌ Erro ao carregar modelo: {e}")
        return False

    def get_ml_prediction(self, dataframe: pd.DataFrame) -> pd.Series:
        """
        Obter predições do modelo ML
        """
        if not ML_AVAILABLE or self.model is None:
            return pd.Series(0, index=dataframe.index)

        try:
            X = self.prepare_ml_data(dataframe)
            X_scaled = self.scaler.transform(X)

            # Predições de probabilidade
            probabilities = self.model.predict_proba(X_scaled)[:, 1]  # Probabilidade da classe 1

            return pd.Series(probabilities, index=dataframe.index)

        except Exception as e:
            logger.error(f"❌ Erro na predição ML: {e}")
            return pd.Series(0, index=dataframe.index)

    def populate_indicators(self, dataframe, metadata):
        """
        Calcular indicadores e treinar modelo se necessário
        """
        # Criar features
        dataframe = self.create_features(dataframe)

        # Verificar se precisa treinar/retreinar modelo
        current_candle = len(dataframe)
        retrain_interval = self.get_int_value(self.retrain_interval)

        if (self.model is None and not self.load_model()) or \
           (current_candle - self.last_train_candle > retrain_interval):

            if self.train_model(dataframe):
                self.last_train_candle = current_candle

        # Obter predições ML
        if ML_AVAILABLE and self.model is not None:
            dataframe['ml_prediction'] = self.get_ml_prediction(dataframe)
        else:
            # Fallback: usar RSI como "predição"
            dataframe['ml_prediction'] = (100 - dataframe['rsi']) / 100

        return dataframe

    def populate_entry_trend(self, dataframe, metadata):
        """
        Sinais de entrada baseados em ML + confirmação técnica
        """
        ml_threshold = self.ml_threshold.value

        if ML_AVAILABLE and self.model is not None:
            # Condições ML
            ml_signal = dataframe['ml_prediction'] > ml_threshold

            # Confirmação técnica
            technical_confirm = (
                (dataframe['rsi'] < 70) &
                (dataframe['bb_percent'] < 0.8) &
                (dataframe['volume_ratio'] > 0.8) &
                (dataframe['ema_trend'] == 1)
            )

            entry_signal = ml_signal & technical_confirm

        else:
            # Fallback: estratégia técnica tradicional
            entry_signal = (
                (dataframe['rsi'] < 30) &
                (dataframe['bb_percent'] < 0.2) &
                (dataframe['macd'] > dataframe['macd_signal']) &
                (dataframe['volume_ratio'] > 1.2) &
                (dataframe['ema_trend'] == 1)
            )

        dataframe.loc[entry_signal, 'enter_long'] = 1
        dataframe.loc[entry_signal, 'enter_tag'] = 'ML_signal' if ML_AVAILABLE else 'technical_fallback'

        return dataframe

    def populate_exit_trend(self, dataframe, metadata):
        """
        Sinais de saída
        """
        if ML_AVAILABLE and self.model is not None:
            # Saída quando ML prediction fica baixa
            exit_signal = (
                (dataframe['ml_prediction'] < 0.3) |
                (dataframe['rsi'] > 80) |
                (dataframe['bb_percent'] > 0.95)
            )
        else:
            # Fallback técnico
            exit_signal = (
                (dataframe['rsi'] > 70) |
                (dataframe['bb_percent'] > 0.8) |
                (dataframe['macd'] < dataframe['macd_signal'])
            )

        dataframe.loc[exit_signal, 'exit_long'] = 1

        return dataframe
