# source: https://raw.githubusercontent.com/4tie/fortiesr/d5cc40760e3cbaff381058a0ea8733d2be469578/user_data/strategies/HermesTestStrategy.py
# Auto-generated by Strategy Lab — Auto-Quant Factory (Omni-Strategy Edition)
# One massive template with Boolean switches for every indicator.
# Hyperopt mutates True/False switches + numeric thresholds to find the best combo.
# Optimise with: freqtrade hyperopt --strategy Github_4tie_fortiesr__HermesTestStrategy__20260709_213639 --spaces buy roi stoploss

from __future__ import annotations
from datetime import datetime
from functools import reduce
import operator

from freqtrade.strategy import CategoricalParameter, IntParameter, DecimalParameter, IStrategy, Trade, stoploss_from_open
from pandas import DataFrame
import talib.abstract as ta
import freqtrade.vendor.qtpylib.indicators as qtpylib
import numpy as np


class Github_4tie_fortiesr__HermesTestStrategy__20260709_213639(IStrategy):
    INTERFACE_VERSION: int = 3

    minimal_roi = {"0": 0.03, "10": 0.02, "30": 0.01, "60": 0}

    stoploss = -0.025
    timeframe = "5m"
    trailing_stop = False
    use_custom_stoploss = True
    process_only_new_candles = True

    # ── Boolean indicator switches (True = active, False = ignored) ───────────
    use_rsi       = CategoricalParameter([True, False], default=True,  space="buy", optimize=True)
    use_macd      = CategoricalParameter([True, False], default=True,  space="buy", optimize=True)
    use_bb        = CategoricalParameter([True, False], default=True,  space="buy", optimize=True)
    use_ema_cross = CategoricalParameter([True, False], default=False, space="buy", optimize=True)
    use_adx       = CategoricalParameter([True, False], default=False, space="buy", optimize=True)
    use_atr       = CategoricalParameter([True, False], default=False, space="buy", optimize=True)
    use_stoch     = CategoricalParameter([True, False], default=False, space="buy", optimize=True)
    use_cci       = CategoricalParameter([True, False], default=False, space="buy", optimize=True)
    use_mfi       = CategoricalParameter([True, False], default=False, space="buy", optimize=True)
    use_willr     = CategoricalParameter([True, False], default=False, space="buy", optimize=True)
    use_obv       = CategoricalParameter([True, False], default=False, space="buy", optimize=True)
    use_sar       = CategoricalParameter([True, False], default=False, space="buy", optimize=True)
    use_natr      = CategoricalParameter([True, False], default=False, space="buy", optimize=True)

    # ── Per-indicator numeric thresholds ─────────────────────────────────────
    rsi_threshold  = IntParameter(20, 50, default=30, space="buy", optimize=True)
    adx_threshold  = IntParameter(15, 40, default=25, space="buy", optimize=True)
    macd_hist_min  = DecimalParameter(0.0, 0.005, default=0.0,   decimals=4, space="buy", optimize=True)
    bb_factor      = DecimalParameter(0.95, 1.0,  default=1.0,   decimals=3, space="buy", optimize=True)
    ema_fast_p     = IntParameter(5, 30,   default=50,  space="buy", optimize=False)
    ema_slow_p     = IntParameter(50, 250, default=200, space="buy", optimize=False)
    stoch_threshold = IntParameter(20, 40, default=30, space="buy", optimize=True)
    cci_threshold  = IntParameter(-100, -50, default=-100, space="buy", optimize=True)
    mfi_threshold  = IntParameter(20, 40, default=30, space="buy", optimize=True)
    willr_threshold = IntParameter(-80, -60, default=-80, space="buy", optimize=True)

    # ── ATR regime window ─────────────────────────────────────────────────────
    atr_window     = IntParameter(10, 60, default=20, space="buy", optimize=True)

    # ── Stepped trailing profit lock-in tiers ─────────────────────────────────
    ts_tier1_trigger = DecimalParameter(0.020, 0.040, default=0.030, decimals=3, space="buy", optimize=True)
    ts_tier1_lock    = DecimalParameter(0.001, 0.010, default=0.003, decimals=3, space="buy", optimize=True)
    ts_tier2_trigger = DecimalParameter(0.050, 0.080, default=0.060, decimals=3, space="buy", optimize=True)
    ts_tier2_lock    = DecimalParameter(0.030, 0.050, default=0.035, decimals=3, space="buy", optimize=True)
    ts_tier3_trigger = DecimalParameter(0.100, 0.180, default=0.120, decimals=3, space="buy", optimize=True)
    ts_tier3_lock    = DecimalParameter(0.060, 0.100, default=0.080, decimals=3, space="buy", optimize=True)

    # ── ATR-based position sizing for winning pairs ───────────────────────────
    # ATR dictionary for each winning pair (pair -> ATR value)
    atr_dict = {}
    target_risk_pct = 0.02  # 2% risk per trade

    # ── Trading window filter (Session Optimizer) ───────────────────────────
    # Blocked hours and days based on historical performance analysis
    blocked_hours = []  # e.g., [0, 1, 2, 3, 4, 5] for early morning UTC
    blocked_days = []   # e.g., [5, 6] for weekends (0=Monday, 6=Sunday)

    def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime,
                        current_rate: float, current_profit: float,
                        after_fill: bool, **kwargs) -> float | None:
        if current_profit >= self.ts_tier3_trigger.value:
            return stoploss_from_open(
                self.ts_tier3_lock.value,
                current_profit,
                is_short=trade.is_short,
                leverage=trade.leverage,
            ) or 1
        if current_profit >= self.ts_tier2_trigger.value:
            return stoploss_from_open(
                self.ts_tier2_lock.value,
                current_profit,
                is_short=trade.is_short,
                leverage=trade.leverage,
            ) or 1
        if current_profit >= self.ts_tier1_trigger.value:
            return stoploss_from_open(
                self.ts_tier1_lock.value,
                current_profit,
                is_short=trade.is_short,
                leverage=trade.leverage,
            ) or 1
        return None

    def custom_stake_amount(self, pair: str, current_time, current_rate: float,
                            proposed_stake: float, min_stake: float | None, max_stake: float,
                            leverage: float, entry_tag: str | None, side: str, **kwargs) -> float:
        """Calculate position size based on ATR to normalize risk across pairs.

        Formula: position_size = base_stake * (target_risk_pct / (ATR / current_price))
        High-volatility pairs get smaller position sizes to keep risk normalized.
        """
        # Division by zero protection
        if current_rate <= 0:
            return proposed_stake
        
        atr = self.atr_dict.get(pair, current_rate * 0.02)  # fallback to 2% of price
        atr_pct = atr / current_rate
        
        # Division by zero protection for ATR
        if atr_pct <= 0:
            return proposed_stake
        
        position_size = proposed_stake * (self.target_risk_pct / atr_pct)
        # Clamp to reasonable bounds (0.5x to 2.0x of base stake)
        position_size = min(max_stake, max(min_stake if min_stake else proposed_stake * 0.5, position_size))
        position_size = min(position_size, proposed_stake * 2.0)
        return position_size

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

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

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

        # EMA 50 / 200
        dataframe["ema_fast"] = ta.EMA(dataframe, timeperiod=self.ema_fast_p.value)
        dataframe["ema_slow"] = ta.EMA(dataframe, timeperiod=self.ema_slow_p.value)

        # ADX
        dataframe["adx"] = ta.ADX(dataframe, timeperiod=14)

        # ATR + rolling median (trend filter)
        dataframe["atr"] = ta.ATR(dataframe, timeperiod=14)
        dataframe["atr_median"] = (
            dataframe["atr"]
            .rolling(window=self.atr_window.value, min_periods=1)
            .median()
        )

        # STOCH (Stochastic Oscillator)
        stoch = ta.STOCH(dataframe, fastk_period=14, slowk_period=3, slowd_period=3)
        dataframe["stoch_slowk"] = stoch["slowk"]
        dataframe["stoch_slowd"] = stoch["slowd"]

        # CCI (Commodity Channel Index)
        dataframe["cci"] = ta.CCI(dataframe, timeperiod=20)

        # MFI (Money Flow Index)
        dataframe["mfi"] = ta.MFI(dataframe, timeperiod=14)

        # WILLR (Williams %R)
        dataframe["willr"] = ta.WILLR(dataframe, timeperiod=14)

        # OBV (On Balance Volume)
        dataframe["obv"] = ta.OBV(dataframe)

        # SAR (Parabolic SAR)
        dataframe["sar"] = ta.SAR(dataframe, acceleration=0.02, maximum=0.2)

        # NATR (Normalized ATR)
        dataframe["natr"] = ta.NATR(dataframe, timeperiod=14)

        return dataframe

    def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        conditions = [dataframe["volume"] > 0]

        # Trading window filter: prevent entries during blocked hours/days
        if self.blocked_hours or self.blocked_days:
            # Get current hour and day from dataframe index (assumes datetime index)
            dataframe["hour"] = dataframe.index.hour
            dataframe["day"] = dataframe.index.dayofweek

            # Block entries during excluded hours
            if self.blocked_hours:
                hour_filter = ~dataframe["hour"].isin(self.blocked_hours)
                conditions.append(hour_filter)

            # Block entries during excluded days
            if self.blocked_days:
                day_filter = ~dataframe["day"].isin(self.blocked_days)
                conditions.append(day_filter)

        if self.use_rsi.value:
            conditions.append(dataframe["rsi"] < self.rsi_threshold.value)

        if self.use_macd.value:
            conditions.append(
                qtpylib.crossed_above(dataframe["macd"], dataframe["macdsignal"])
                & (dataframe["macdhist"] > self.macd_hist_min.value)
            )

        if self.use_bb.value:
            conditions.append(
                dataframe["close"] <= dataframe["bb_lowerband"] * self.bb_factor.value
            )

        if self.use_ema_cross.value:
            conditions.append(dataframe["ema_fast"] > dataframe["ema_slow"])

        if self.use_adx.value:
            conditions.append(dataframe["adx"] > self.adx_threshold.value)

        if self.use_atr.value:
            conditions.append(dataframe["atr"] > dataframe["atr_median"])

        if self.use_stoch.value:
            conditions.append(dataframe["stoch_slowk"] < self.stoch_threshold.value)

        if self.use_cci.value:
            conditions.append(dataframe["cci"] < self.cci_threshold.value)

        if self.use_mfi.value:
            conditions.append(dataframe["mfi"] < self.mfi_threshold.value)

        if self.use_willr.value:
            conditions.append(dataframe["willr"] < self.willr_threshold.value)

        if self.use_obv.value:
            conditions.append(dataframe["obv"] > dataframe["obv"].rolling(20).mean())

        if self.use_sar.value:
            conditions.append(dataframe["close"] > dataframe["sar"])

        if self.use_natr.value:
            conditions.append(dataframe["natr"] > dataframe["natr"].rolling(20).mean())

        # Need at least one signal beyond the volume guard
        if len(conditions) > 1:
            combined = reduce(operator.and_, conditions)
            dataframe.loc[combined, "enter_long"] = 1

        return dataframe

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