# source: https://raw.githubusercontent.com/4tie/fortiesr/d5cc40760e3cbaff381058a0ea8733d2be469578/user_data/strategies/EnsembleFactory.py
# directory_url: https://github.com/4tie/fortiesr/blob/main/user_data/strategies/
# User: 4tie
# Repository: fortiesr
# --------------------# Auto-generated by Strategy Lab — Auto-Quant Factory (Alpha Ensemble Edition)
# Entry logic uses weighted consensus voting across RSI, MACD, and BB signals.
# Optimise with: freqtrade hyperopt --strategy Github_4tie_fortiesr__EnsembleFactory__20260709_213639 --spaces buy roi stoploss
#
# Live weight updates: write user_data/ensemble_weights.json to change weights
# without restarting freqtrade (changes take effect on the next candle).

import json
from pathlib import Path

from freqtrade.strategy import DecimalParameter, IntParameter, IStrategy
from pandas import DataFrame
import talib.abstract as ta
import freqtrade.vendor.qtpylib.indicators as qtpylib


class Github_4tie_fortiesr__EnsembleFactory__20260709_213639(IStrategy):
    INTERFACE_VERSION: int = 3

    minimal_roi = {
        "0": 0.10,
        "30": 0.05,
        "60": 0.02,
        "120": 0,
    }

    stoploss = -0.05
    timeframe = "5m"
    trailing_stop = False
    process_only_new_candles = True

    # ── Alpha Weights — optimised by hyperopt (buy space) ────────────────────
    # Setting a weight to 0.0 effectively switches that signal off.
    rsi_weight  = DecimalParameter(0.0, 1.0, default=0.4, decimals=2, space="buy", optimize=True)
    macd_weight = DecimalParameter(0.0, 1.0, default=0.3, decimals=2, space="buy", optimize=True)
    bb_weight   = DecimalParameter(0.0, 1.0, default=0.3, decimals=2, space="buy", optimize=True)

    # ── Consensus threshold — how high the weighted score must be to enter ───
    consensus_threshold = DecimalParameter(0.1, 0.9, default=0.5, decimals=2,
                                           space="buy", optimize=True)

    # ── Individual signal parameters ─────────────────────────────────────────
    rsi_oversold  = IntParameter(20, 40, default=30, space="buy", optimize=True)
    rsi_period    = IntParameter(10, 20, default=14, space="buy", optimize=False)

    # ── Live-weight helper ────────────────────────────────────────────────────

    def _load_live_weights(self):
        """Attempt to read override weights from ensemble_weights.json.

        Falls back to the hyperopt-optimised DecimalParameter values when the
        file is absent, malformed, or any key is missing.
        """
        try:
            p = Path(self.config.get("user_data_dir", ".")) / "ensemble_weights.json"
            if p.exists():
                cfg = json.loads(p.read_text(encoding="utf-8"))
                return (
                    float(cfg.get("rsi_weight",          self.rsi_weight.value)),
                    float(cfg.get("macd_weight",         self.macd_weight.value)),
                    float(cfg.get("bb_weight",           self.bb_weight.value)),
                    float(cfg.get("consensus_threshold", self.consensus_threshold.value)),
                )
        except Exception as exc:
            logger.warning("Generator | failed to load ensemble_weights.json: %s", exc)
        return (
            self.rsi_weight.value,
            self.macd_weight.value,
            self.bb_weight.value,
            self.consensus_threshold.value,
        )

    # ── Indicator computation ─────────────────────────────────────────────────

    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        # RSI
        dataframe["rsi"] = ta.RSI(dataframe, timeperiod=self.rsi_period.value)

        # MACD (fast=12, slow=26, signal=9)
        macd = ta.MACD(dataframe, fastperiod=12, slowperiod=26, signalperiod=9)
        dataframe["macd"]       = macd["macd"]
        dataframe["macdsignal"] = macd["macdsignal"]
        dataframe["macdhist"]   = macd["macdhist"]

        # Bollinger Bands (period=20, stddev=2)
        bollinger = qtpylib.bollinger_bands(
            qtpylib.typical_price(dataframe), window=20, stds=2
        )
        dataframe["bb_lowerband"] = bollinger["lower"]
        dataframe["bb_upperband"] = bollinger["upper"]

        return dataframe

    # ── Weighted consensus entry logic ────────────────────────────────────────

    def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        rsi_w, macd_w, bb_w, threshold = self._load_live_weights()

        # ── Binary votes (1 = bullish signal present, 0 = absent) ─────────
        rsi_vote  = (dataframe["rsi"] < self.rsi_oversold.value).astype(float)
        macd_vote = qtpylib.crossed_above(
            dataframe["macd"], dataframe["macdsignal"]
        ).astype(float)
        bb_vote   = (dataframe["close"] < dataframe["bb_lowerband"]).astype(float)

        # ── Weighted, normalised consensus score ───────────────────────────
        total_weight = rsi_w + macd_w + bb_w
        if total_weight > 0:
            score = (rsi_vote * rsi_w + macd_vote * macd_w + bb_vote * bb_w) / total_weight
        else:
            score = dataframe["close"] * 0  # all weights zero → never enter

        # ── Enter long when consensus clears the threshold ─────────────────
        dataframe.loc[
            (score >= threshold) & (dataframe["volume"] > 0),
            "enter_long",
        ] = 1

        return dataframe

    # ── Exit stub (relies on ROI / stoploss) ──────────────────────────────────

    def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        return dataframe
