# source: https://raw.githubusercontent.com/NAK09/Cryptocurrency-Trading-Project/88ecc8bd99ba6fbd5a5b879f29c266ef082d1340/okx-v157-candidate/user_data/strategies/KhoaAnnualGrowthV157.py
# directory_url: https://github.com/NAK09/Cryptocurrency-Trading-Project/blob/main/okx-v157-candidate/user_data/strategies/
# User: NAK09
# Repository: Cryptocurrency-Trading-Project
# --------------------﻿from __future__ import annotations

import logging
from datetime import datetime
from typing import Any

import numpy as np
import talib.abstract as ta
from pandas import DataFrame

from freqtrade.persistence import Trade
from freqtrade.strategy import (
    BooleanParameter,
    CategoricalParameter,
    DecimalParameter,
    IStrategy,
    IntParameter,
    merge_informative_pair,
    stoploss_from_absolute,
)

logger = logging.getLogger(__name__)

class Github_NAK09_Cryptocurrency_Trading_Project__KhoaAnnualGrowthV157__20261009_163020(IStrategy):
    """Standalone V122RiskTune. Includes V117/V118/RiskManaged logic in this file.

    This file intentionally imports no previous KhoaAnnualGrowthVxx strategy.
    """

    """V107 plus a macro-bull overheat guard.

    The guard is intentionally dynamic: it blocks full-size macro-bull entries
    only when BTC is already overheated and the traded pair is also stretched.
    This targets weak blow-off entries without hard-coding dates or years.
    """

    INTERFACE_VERSION = 3

    timeframe = "1h"
    # Binance returns at most 1000 OHLCV rows per request. 999 plus the
    # currently open candle stays within one request while retaining ample
    # warmup for the longest 720-candle rolling window.
    startup_candle_count = 999
    can_short = False

    stoploss = -0.150
    minimal_roi = {"0": 0.447, "351": 0.139, "707": 0.078, "1620": 0.0}
    trailing_stop = True
    use_custom_stoploss = True
    trailing_stop_positive = 0.149
    trailing_stop_positive_offset = 0.163
    trailing_only_offset_is_reached = False
    process_only_new_candles = True
    use_exit_signal = True
    exit_profit_only = False
    ignore_roi_if_entry_signal = False

    execution_guard_enabled = True
    execution_guard_max_spread = 0.0030
    execution_guard_max_rate_deviation = 0.0040
    execution_guard_max_book_impact = 0.0040
    execution_guard_book_depth = 5
    execution_guard_max_candle_age_minutes = 180
    execution_guard_max_red_candle_drop = 0.120
    execution_guard_max_panic_range = 0.180

    buy_btc_ema50_mult = DecimalParameter(0.950, 1.020, default=0.997, decimals=3, space="buy", optimize=False)
    buy_btc_ema200_mult = DecimalParameter(0.980, 1.080, default=0.996, decimals=3, space="buy", optimize=False)
    buy_btc_slope_floor = DecimalParameter(-0.010, 0.010, default=0.003, decimals=3, space="buy", optimize=False)
    buy_btc_rsi_min = IntParameter(45, 58, default=52, space="buy", optimize=False)
    buy_btc_extension_mult = DecimalParameter(1.030, 1.140, default=1.131, decimals=3, space="buy", optimize=False)
    buy_btc_extension_rsi = IntParameter(52, 62, default=62, space="buy", optimize=False)
    buy_btc_change3_floor = DecimalParameter(0.035, 0.120, default=0.094, decimals=3, space="buy", optimize=False)

    buy_p4h_ema200_mult = DecimalParameter(0.920, 1.020, default=1.007, decimals=3, space="buy", optimize=False)
    buy_p4h_ema100_mult = DecimalParameter(0.940, 1.020, default=1.014, decimals=3, space="buy", optimize=False)
    buy_p4h_slope_floor = DecimalParameter(-0.015, 0.010, default=0.009, decimals=3, space="buy", optimize=False)
    buy_p4h_rsi_min = IntParameter(46, 58, default=54, space="buy", optimize=False)
    buy_p4h_adx_min = IntParameter(12, 26, default=18, space="buy", optimize=False)

    buy_local_ema200_mult = DecimalParameter(0.950, 1.030, default=0.958, decimals=3, space="buy", optimize=False)
    buy_local_slope_floor = DecimalParameter(-0.015, 0.010, default=0.006, decimals=3, space="buy", optimize=False)
    buy_local_rsi_min = IntParameter(45, 58, default=50, space="buy", optimize=False)
    buy_local_rsi_max = IntParameter(66, 82, default=73, space="buy", optimize=False)
    buy_local_adx_min = IntParameter(10, 24, default=13, space="buy", optimize=False)
    buy_atr_min = DecimalParameter(0.003, 0.012, default=0.004, decimals=3, space="buy", optimize=False)
    buy_atr_max = DecimalParameter(0.045, 0.100, default=0.057, decimals=3, space="buy", optimize=False)

    buy_pullback_ema_buffer = DecimalParameter(1.000, 1.025, default=1.023, decimals=3, space="buy", optimize=False)
    buy_pullback_volume_min = DecimalParameter(0.55, 1.25, default=1.19, decimals=2, space="buy", optimize=False)
    buy_retracement_max = DecimalParameter(0.080, 0.260, default=0.113, decimals=3, space="buy", optimize=False)
    buy_body_ratio_min = DecimalParameter(0.120, 0.420, default=0.17, decimals=2, space="buy", optimize=False)
    buy_opportunistic_enabled = BooleanParameter(default=False, space="buy", optimize=False)

    buy_breakout_enabled = BooleanParameter(default=False, space="buy", optimize=False)
    buy_breakout_window = CategoricalParameter([72, 96, 168], default=168, space="buy", optimize=False)
    buy_breakout_volume_min = DecimalParameter(0.70, 1.80, default=1.51, decimals=2, space="buy", optimize=False)
    buy_breakout_rsi_min = IntParameter(50, 64, default=57, space="buy", optimize=False)
    buy_breakout_rsi_max = IntParameter(68, 86, default=75, space="buy", optimize=False)
    buy_breakout_close_location = DecimalParameter(0.52, 0.85, default=0.52, decimals=2, space="buy", optimize=False)

    buy_macro_enabled = BooleanParameter(default=True, space="buy", optimize=False)
    buy_macro_rsi_min = IntParameter(48, 62, default=62, space="buy", optimize=False)
    buy_macro_rsi_max = IntParameter(68, 88, default=82, space="buy", optimize=False)
    buy_macro_adx_min = IntParameter(14, 30, default=16, space="buy", optimize=False)
    buy_macro_volume_min = DecimalParameter(0.45, 1.20, default=0.51, decimals=2, space="buy", optimize=False)
    buy_macro_overheat_guard_enabled = BooleanParameter(default=True, space="buy", optimize=False)
    buy_macro_overheat_btc_rsi = IntParameter(74, 88, default=80, space="buy", optimize=False)
    buy_macro_overheat_btc_change3 = DecimalParameter(0.015, 0.100, default=0.025, decimals=3, space="buy", optimize=False)
    buy_macro_overheat_p4h_rsi = IntParameter(64, 82, default=70, space="buy", optimize=False)
    buy_macro_overheat_p4h_change3 = DecimalParameter(0.005, 0.080, default=0.02, decimals=3, space="buy", optimize=False)
    buy_macro_overheat_close_location = DecimalParameter(0.25, 0.70, default=1.0, decimals=2, space="buy", optimize=False)

    buy_reentry_enabled = BooleanParameter(default=True, space="buy", optimize=False)
    buy_reentry_dump24 = DecimalParameter(0.035, 0.120, default=0.064, decimals=3, space="buy", optimize=False)
    buy_reentry_dump72 = DecimalParameter(0.060, 0.200, default=0.082, decimals=3, space="buy", optimize=False)
    buy_reentry_low_buffer = DecimalParameter(1.010, 1.080, default=1.023, decimals=3, space="buy", optimize=False)
    buy_reentry_max_from_low = DecimalParameter(1.050, 1.180, default=1.085, decimals=3, space="buy", optimize=False)
    buy_reentry_volume_ratio = DecimalParameter(0.800, 2.500, default=1.81, decimals=2, space="buy", optimize=False)
    buy_reentry_rsi7 = IntParameter(30, 48, default=45, space="buy", optimize=False)
    buy_reentry_rsi14 = IntParameter(36, 55, default=51, space="buy", optimize=False)
    buy_reentry_btc_change3_floor = DecimalParameter(0.080, 0.240, default=0.189, decimals=3, space="buy", optimize=False)
    buy_reentry_btc_ema200_floor = DecimalParameter(0.650, 0.940, default=0.768, decimals=3, space="buy", optimize=False)
    buy_reentry_stake_pct = DecimalParameter(0.050, 1.000, default=0.92, decimals=2, space="buy", optimize=False)
    buy_reentry_full_btc_slope_min = DecimalParameter(-0.020, 0.015, default=-0.004, decimals=3, space="buy", optimize=False)
    buy_reentry_full_p4h_slope_min = DecimalParameter(-0.020, 0.015, default=-0.002, decimals=3, space="buy", optimize=False)
    buy_reentry_weak_enabled = BooleanParameter(default=False, space="buy", optimize=False)
    buy_reentry_weak_stake_pct = DecimalParameter(0.050, 0.700, default=0.18, decimals=2, space="buy", optimize=False)

    buy_bottom_enabled = BooleanParameter(default=False, space="buy", optimize=False)
    buy_bottom_bb_std = DecimalParameter(1.70, 2.80, default=2.15, decimals=2, space="buy", optimize=False)
    buy_bottom_rsi7_max = IntParameter(16, 36, default=29, space="buy", optimize=False)
    buy_bottom_rsi14_max = IntParameter(24, 44, default=35, space="buy", optimize=False)
    buy_bottom_reclaim_rsi7 = IntParameter(25, 48, default=36, space="buy", optimize=False)
    buy_bottom_volume_mult = DecimalParameter(0.75, 2.80, default=1.2, decimals=2, space="buy", optimize=False)
    buy_bottom_pinbar_ratio = DecimalParameter(0.35, 0.75, default=0.52, decimals=2, space="buy", optimize=False)
    buy_bottom_max_from_low = DecimalParameter(1.025, 1.140, default=1.075, decimals=3, space="buy", optimize=False)
    buy_bottom_btc_change3_floor = DecimalParameter(0.080, 0.260, default=0.16, decimals=3, space="buy", optimize=False)
    buy_bottom_btc_ema200_floor = DecimalParameter(0.620, 0.920, default=0.74, decimals=3, space="buy", optimize=False)
    buy_bottom_stake_pct = DecimalParameter(0.050, 0.450, default=0.18, decimals=2, space="buy", optimize=False)

    buy_rebound_enabled = BooleanParameter(default=True, space="buy", optimize=False)
    buy_rebound_btc_change3_floor = DecimalParameter(0.060, 0.180, default=0.115, decimals=3, space="buy", optimize=False)
    buy_rebound_btc_ema200_floor = DecimalParameter(0.650, 0.880, default=0.75, decimals=3, space="buy", optimize=False)
    buy_rebound_p4h_rsi_min = IntParameter(52, 70, default=58, space="buy", optimize=False)
    buy_rebound_p4h_change3_min = DecimalParameter(0.000, 0.060, default=0.018, decimals=3, space="buy", optimize=False)
    buy_rebound_rsi_min = IntParameter(52, 68, default=58, space="buy", optimize=False)
    buy_rebound_rsi_max = IntParameter(70, 86, default=82, space="buy", optimize=False)
    buy_rebound_volume_min = DecimalParameter(0.70, 2.40, default=1.15, decimals=2, space="buy", optimize=False)
    buy_rebound_close_location = DecimalParameter(0.48, 0.78, default=0.57, decimals=2, space="buy", optimize=False)
    buy_rebound_max_from_low = DecimalParameter(1.050, 1.220, default=1.15, decimals=3, space="buy", optimize=False)
    buy_rebound_stake_pct = DecimalParameter(0.150, 1.000, default=1.0, decimals=2, space="buy", optimize=False)
    buy_rebound_full_btc_slope_min = DecimalParameter(-0.020, 0.015, default=-0.002, decimals=3, space="buy", optimize=False)
    buy_rebound_full_btc_rsi_min = IntParameter(38, 62, default=48, space="buy", optimize=False)
    buy_rebound_full_p4h_slope_min = DecimalParameter(-0.020, 0.015, default=0.002, decimals=3, space="buy", optimize=False)
    buy_rebound_weak_enabled = BooleanParameter(default=False, space="buy", optimize=False)
    buy_rebound_weak_stake_pct = DecimalParameter(0.050, 0.800, default=0.3, decimals=2, space="buy", optimize=False)
    buy_medium_pullback_enabled = BooleanParameter(default=False, space="buy", optimize=False)

    sell_macro_btc_change3_floor = DecimalParameter(0.050, 0.140, default=0.08, decimals=3, space="sell", optimize=False)
    sell_macro_btc_ema200_mult = DecimalParameter(0.940, 1.020, default=0.98, decimals=3, space="sell", optimize=False)
    sell_macro_p4h_ema100_mult = DecimalParameter(0.970, 1.030, default=1.0, decimals=3, space="sell", optimize=False)
    sell_macro_p4h_ema50_ema100_mult = DecimalParameter(0.960, 1.020, default=0.99, decimals=3, space="sell", optimize=False)
    sell_macro_p4h_slope_floor = DecimalParameter(-0.005, 0.006, default=0.0, decimals=3, space="sell", optimize=False)
    sell_soft_p4h_ema50_mult = DecimalParameter(0.980, 1.020, default=1.0, decimals=3, space="sell", optimize=False)
    sell_soft_p4h_slope_max = DecimalParameter(-0.015, 0.003, default=0.0, decimals=3, space="sell", optimize=False)
    sell_soft_ema100_mult = DecimalParameter(0.980, 1.020, default=1.0, decimals=3, space="sell", optimize=False)
    sell_soft_rsi_max = IntParameter(38, 52, default=45, space="sell", optimize=False)
    sell_hard_btc_change3 = DecimalParameter(0.080, 0.180, default=0.12, decimals=3, space="sell", optimize=False)
    sell_hard_p4h_ema200_mult = DecimalParameter(0.940, 1.010, default=0.97, decimals=3, space="sell", optimize=False)
    sell_hard_rsi_max = IntParameter(30, 45, default=38, space="sell", optimize=False)
    sell_macro_loss_guard_enabled = BooleanParameter(default=True, space="sell", optimize=False)
    sell_macro_loss_guard_profit = DecimalParameter(-0.100, -0.035, default=-0.045, decimals=3, space="sell", optimize=False)
    sell_macro_loss_guard_btc_change3 = DecimalParameter(0.025, 0.100, default=0.025, decimals=3, space="sell", optimize=False)
    sell_macro_loss_guard_p4h_ema50_mult = DecimalParameter(0.970, 1.005, default=1.005, decimals=3, space="sell", optimize=False)
    sell_macro_loss_guard_rsi_max = IntParameter(38, 55, default=55, space="sell", optimize=False)

    @property
    def protections(self) -> list[dict[str, Any]]:
        return [
            {"method": "CooldownPeriod", "stop_duration_candles": 4},
            {
                "method": "StoplossGuard",
                "lookback_period_candles": 96,
                "trade_limit": 3,
                "stop_duration_candles": 48,
                "only_per_pair": True,
            },
        ]

    def informative_pairs(self) -> list[tuple[str, str]]:
        pairs = self.dp.current_whitelist() if self.dp else []
        informative = [] if self.timeframe == "4h" else [(pair, "4h") for pair in pairs]
        informative.append(("BTC/USDT", "1d"))
        return sorted(set(informative))

    @staticmethod
    def _safe_div(numerator, denominator):
        return numerator / denominator.replace(0, np.nan)

    @staticmethod
    def _timeframe_minutes(timeframe: str) -> int:
        unit = timeframe[-1]
        amount = int(timeframe[:-1])
        if unit == "m":
            return amount
        if unit == "h":
            return amount * 60
        if unit == "d":
            return amount * 1440
        return 60

    def _candles_for_hours(self, hours: float, minimum: int = 1) -> int:
        minutes = self._timeframe_minutes(self.timeframe)
        return max(minimum, int(round(hours * 60 / minutes)))

    def _execution_guard_active(self) -> bool:
        if not self.execution_guard_enabled or not self.dp:
            return False
        runmode = getattr(getattr(self.dp, "runmode", None), "value", "")
        return runmode in {"dry_run", "live"}

    @staticmethod
    def _estimate_book_impact(asks: list, amount: float) -> float | None:
        if not asks or amount <= 0:
            return None
        remaining = float(amount)
        cost = 0.0
        filled = 0.0
        best_ask = float(asks[0][0])
        for price, size in asks:
            take = min(remaining, float(size))
            cost += take * float(price)
            filled += take
            remaining -= take
            if remaining <= 0:
                break
        if filled <= 0 or remaining > 0:
            return None
        average_price = cost / filled
        return max(0.0, (average_price - best_ask) / best_ask)

    def _latest_candle_guard_ok(self, pair: str, current_time: datetime, entry_tag: str | None) -> bool:
        try:
            dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
            if dataframe is None or dataframe.empty:
                return True
            candle = dataframe.iloc[-1]
            last_date = candle.get("date")
            if hasattr(last_date, "to_pydatetime"):
                last_date = last_date.to_pydatetime()
            if isinstance(last_date, datetime):
                check_time = current_time
                if last_date.tzinfo is None and check_time.tzinfo is not None:
                    last_date = last_date.replace(tzinfo=check_time.tzinfo)
                if last_date.tzinfo is not None and check_time.tzinfo is None:
                    check_time = check_time.replace(tzinfo=last_date.tzinfo)
                age_minutes = abs((check_time - last_date).total_seconds()) / 60
                if age_minutes > self.execution_guard_max_candle_age_minutes:
                    logger.warning(
                        "entry blocked by stale candle guard pair=%s age_minutes=%.1f tag=%s",
                        pair,
                        age_minutes,
                        entry_tag,
                    )
                    return False
            open_rate = float(candle.get("open", 0) or 0)
            high = float(candle.get("high", 0) or 0)
            low = float(candle.get("low", 0) or 0)
            close = float(candle.get("close", 0) or 0)
            volume = float(candle.get("volume", 0) or 0)
            if min(open_rate, high, low, close) <= 0 or volume < 0 or high < max(open_rate, close, low) or low > min(open_rate, close, high):
                logger.warning("entry blocked by OHLCV sanity guard pair=%s tag=%s", pair, entry_tag)
                return False
            candle_range = (high - low) / close if close > 0 else 0.0
            red_drop = (open_rate - close) / open_rate if open_rate > 0 and close < open_rate else 0.0
            close_location = (close - low) / (high - low) if high > low else 0.5
            if candle_range > self.execution_guard_max_panic_range and close_location < 0.25:
                logger.warning(
                    "entry blocked by panic range guard pair=%s range=%.4f close_location=%.2f tag=%s",
                    pair,
                    candle_range,
                    close_location,
                    entry_tag,
                )
                return False
            if red_drop > self.execution_guard_max_red_candle_drop and close_location < 0.45:
                logger.warning(
                    "entry blocked by bleeding candle guard pair=%s red_drop=%.4f close_location=%.2f tag=%s",
                    pair,
                    red_drop,
                    close_location,
                    entry_tag,
                )
                return False
        except Exception as exc:
            logger.warning("latest candle guard skipped pair=%s tag=%s error=%s", pair, entry_tag, exc)
        return True

    def confirm_trade_entry(
        self,
        pair: str,
        order_type: str,
        amount: float,
        rate: float,
        time_in_force: str,
        current_time: datetime,
        entry_tag: str | None,
        side: str,
        **kwargs: Any,
    ) -> bool:
        if side != "long" or not self._execution_guard_active():
            return True
        try:
            if not self._latest_candle_guard_ok(pair, current_time, entry_tag):
                return False
            orderbook = self.dp.orderbook(pair, self.execution_guard_book_depth)
            bids = orderbook.get("bids") or []
            asks = orderbook.get("asks") or []
            if not bids or not asks:
                return True
            best_bid = float(bids[0][0])
            best_ask = float(asks[0][0])
            mid = (best_bid + best_ask) / 2
            spread = (best_ask - best_bid) / mid if mid > 0 else 0.0
            rate_deviation = abs(best_ask - float(rate)) / float(rate) if rate else 0.0
            impact = self._estimate_book_impact(asks, amount)
            if spread > self.execution_guard_max_spread:
                logger.warning("entry blocked by spread guard pair=%s spread=%.4f tag=%s", pair, spread, entry_tag)
                return False
            if rate_deviation > self.execution_guard_max_rate_deviation:
                logger.warning(
                    "entry blocked by rate guard pair=%s deviation=%.4f rate=%.8f ask=%.8f tag=%s",
                    pair,
                    rate_deviation,
                    rate,
                    best_ask,
                    entry_tag,
                )
                return False
            if impact is not None and impact > self.execution_guard_max_book_impact:
                logger.warning("entry blocked by impact guard pair=%s impact=%.4f tag=%s", pair, impact, entry_tag)
                return False
        except Exception as exc:
            logger.warning("entry execution guard skipped pair=%s tag=%s error=%s", pair, entry_tag, exc)
        return True

    def _v117_custom_stake_amount(
        self,
        pair: str,
        current_time: datetime,
        current_rate: float,
        proposed_stake: float,
        min_stake: float | None,
        max_stake: float,
        leverage: float,
        entry_tag: str | None,
        side: str,
        **kwargs: Any,
    ) -> float:
        stake = proposed_stake
        if entry_tag == "post_capitulation_reclaim":
            stake = proposed_stake * float(self.buy_reentry_stake_pct.value)
        elif entry_tag == "post_capitulation_reclaim_weak":
            stake = proposed_stake * float(self.buy_reentry_weak_stake_pct.value)
        elif entry_tag == "bottom_reclaim":
            stake = proposed_stake * float(self.buy_bottom_stake_pct.value)
        elif entry_tag == "macro_bear_rebound":
            stake = proposed_stake * float(self.buy_rebound_stake_pct.value)
        elif entry_tag == "macro_bear_rebound_weak":
            stake = proposed_stake * float(self.buy_rebound_weak_stake_pct.value)
        stake = min(float(stake), float(max_stake))
        if min_stake is not None and stake > 0:
            stake = max(stake, float(min_stake))
        return stake

    def _v117_custom_stoploss(
        self,
        pair: str,
        trade: Trade,
        current_time: datetime,
        current_rate: float,
        current_profit: float,
        after_fill: bool,
        **kwargs: Any,
    ) -> float | None:
        if trade.enter_tag in {
            "post_capitulation_reclaim",
            "post_capitulation_reclaim_weak",
            "bottom_reclaim",
            "macro_bear_rebound",
            "macro_bear_rebound_weak",
        }:
            if current_profit > 0.110:
                return 0.035
            if current_profit > 0.055:
                return 0.010
            try:
                dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
                candle = dataframe.iloc[-1]
                atr = float(candle.get("atr", trade.open_rate * 0.025))
                recent_low = float(candle.get("panic_low_recent", trade.open_rate * 0.90))
                stop_price = max(recent_low - 0.5 * atr, trade.open_rate * 0.90)
                stop_price = min(stop_price, trade.open_rate * 0.985)
                return stoploss_from_absolute(stop_price, current_rate, is_short=False, leverage=1.0)
            except (AttributeError, IndexError, KeyError, TypeError, ValueError):
                return 0.10
        if trade.enter_tag in {"trend_strong_pullback", "bull_continuation_breakout"} and current_profit > 0.12:
            return 0.040
        if trade.enter_tag == "trend_medium_pullback" and current_profit > 0.09:
            return 0.030
        return None

    def _v117_custom_exit(
        self,
        pair: str,
        trade: Trade,
        current_time: datetime,
        current_rate: float,
        current_profit: float,
        **kwargs: Any,
    ) -> str | bool | None:
        if trade.enter_tag != "macro_bull_hold" or not bool(self.sell_macro_loss_guard_enabled.value):
            return None
        if current_profit > float(self.sell_macro_loss_guard_profit.value):
            return None
        try:
            dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
            candle = dataframe.iloc[-1]
            btc_break = float(candle.get("btc_change3_1d", 0.0)) < -float(self.sell_macro_loss_guard_btc_change3.value)
            p4h_break = float(candle.get("close_4h", 0.0)) < (
                float(candle.get("p4h_ema50_4h", 0.0)) * float(self.sell_macro_loss_guard_p4h_ema50_mult.value)
            )
            local_break = (
                float(candle.get("close", 0.0)) < float(candle.get("ema100", 0.0))
                and float(candle.get("rsi", 100.0)) < int(self.sell_macro_loss_guard_rsi_max.value)
                and float(candle.get("macd", 0.0)) < float(candle.get("macdsignal", 0.0))
            )
            if btc_break or (p4h_break and local_break):
                return "macro_loss_guard"
        except (AttributeError, IndexError, KeyError, TypeError, ValueError):
            return None
        return None

    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        ema_slope_window = self._candles_for_hours(8, minimum=2)
        atr_mean_window = self._candles_for_hours(168, minimum=24)
        bottom_recent_window = self._candles_for_hours(48, minimum=8)
        return_24h_window = self._candles_for_hours(24, minimum=2)
        return_72h_window = self._candles_for_hours(72, minimum=4)
        rolling_7d_window = self._candles_for_hours(24 * 7, minimum=24)
        rolling_30d_window = self._candles_for_hours(24 * 30, minimum=120)
        panic_recent_window = self._candles_for_hours(72, minimum=8)
        cap_recent_window = self._candles_for_hours(24, minimum=4)
        range_high_window = self._candles_for_hours(12, minimum=3)

        dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14)
        dataframe["rsi7"] = ta.RSI(dataframe, timeperiod=7)
        dataframe["adx"] = ta.ADX(dataframe, timeperiod=14)
        for period in (8, 20, 50, 100, 200):
            dataframe[f"ema{period}"] = ta.EMA(dataframe, timeperiod=period)
        dataframe["ema50_slope"] = dataframe["ema50"] - dataframe["ema50"].shift(ema_slope_window)
        dataframe["ema50_slope_pct"] = self._safe_div(dataframe["ema50"], dataframe["ema50"].shift(ema_slope_window)) - 1.0

        macd = ta.MACD(dataframe)
        dataframe["macd"] = macd["macd"]
        dataframe["macdsignal"] = macd["macdsignal"]
        dataframe["macdhist"] = macd["macdhist"]
        dataframe["macdhist_rising_2"] = (
            (dataframe["macdhist"] > dataframe["macdhist"].shift(1))
            & (dataframe["macdhist"].shift(1) > dataframe["macdhist"].shift(2))
        )

        dataframe["atr"] = ta.ATR(dataframe, timeperiod=14)
        dataframe["atr_pct"] = self._safe_div(dataframe["atr"], dataframe["close"])
        dataframe["atr_pct_mean_168"] = dataframe["atr_pct"].rolling(atr_mean_window).mean()
        dataframe["atr_spike"] = dataframe["atr_pct"] > dataframe["atr_pct_mean_168"] * 1.35
        dataframe["volume_mean"] = dataframe["volume"].rolling(30).mean()
        dataframe["volume_ratio"] = self._safe_div(dataframe["volume"], dataframe["volume_mean"])
        dataframe["range"] = dataframe["high"] - dataframe["low"]
        dataframe["body"] = (dataframe["close"] - dataframe["open"]).abs()
        dataframe["body_ratio"] = self._safe_div(dataframe["body"], dataframe["range"])
        dataframe["close_location"] = self._safe_div(dataframe["close"] - dataframe["low"], dataframe["range"])
        dataframe["lower_wick_ratio"] = self._safe_div(dataframe[["open", "close"]].min(axis=1) - dataframe["low"], dataframe["range"])

        bbands = ta.BBANDS(
            dataframe,
            timeperiod=20,
            nbdevup=float(self.buy_bottom_bb_std.value),
            nbdevdn=float(self.buy_bottom_bb_std.value),
            matype=0,
        )
        dataframe["bb_lower"] = bbands["lowerband"]
        dataframe["bb_mid"] = bbands["middleband"]
        dataframe["bb_upper"] = bbands["upperband"]
        dataframe["bb_width"] = self._safe_div(dataframe["bb_upper"] - dataframe["bb_lower"], dataframe["bb_mid"])
        dataframe["bottom_penetration"] = (dataframe["close"] < dataframe["bb_lower"]) | (dataframe["low"] < dataframe["bb_lower"])
        dataframe["bottom_oversold"] = (
            (dataframe["rsi7"] < int(self.buy_bottom_rsi7_max.value))
            | (dataframe["rsi"] < int(self.buy_bottom_rsi14_max.value))
        )
        dataframe["bottom_capitulation"] = (
            (dataframe["volume_ratio"] > float(self.buy_bottom_volume_mult.value))
            | dataframe["atr_spike"]
            | (dataframe["lower_wick_ratio"] > float(self.buy_bottom_pinbar_ratio.value))
        )
        dataframe["bottom_absorbed"] = (
            (dataframe["close"] > dataframe["open"])
            | (dataframe["lower_wick_ratio"] > float(self.buy_bottom_pinbar_ratio.value))
            | (dataframe["close_location"] > 0.50)
        )
        dataframe["bottom_seed"] = (
            dataframe["bottom_penetration"]
            & dataframe["bottom_oversold"]
            & dataframe["bottom_capitulation"]
            & dataframe["bottom_absorbed"]
        )
        dataframe["bottom_seed_recent"] = dataframe["bottom_seed"].rolling(bottom_recent_window).max() > 0

        dataframe["high72"] = dataframe["high"].rolling(72).max().shift(1)
        dataframe["high96"] = dataframe["high"].rolling(96).max().shift(1)
        dataframe["high168"] = dataframe["high"].rolling(168).max().shift(1)
        dataframe["low96"] = dataframe["low"].rolling(96).min().shift(1)
        dataframe["retracement96"] = self._safe_div(dataframe["high96"] - dataframe["close"], dataframe["high96"] - dataframe["low96"])
        dataframe["low5"] = dataframe["low"].rolling(5).min()

        dataframe["return_24h"] = dataframe["close"].pct_change(return_24h_window)
        dataframe["return_72h"] = dataframe["close"].pct_change(return_72h_window)
        dataframe["rolling_low_7d"] = dataframe["low"].rolling(rolling_7d_window).min()
        dataframe["rolling_low_30d"] = dataframe["low"].rolling(rolling_30d_window).min()
        dataframe["panic_low_recent"] = dataframe["low"].rolling(panic_recent_window).min()
        dataframe["range_high_12"] = dataframe["high"].rolling(range_high_window).max().shift(1)
        dataframe["big_dump_candle"] = (
            (dataframe["close"] / dataframe["open"] - 1.0 < -0.035)
            | ((dataframe["range"] > dataframe["atr"] * 2.3) & (dataframe["close_location"] < 0.35))
        )
        dataframe["panic_seed"] = (
            (dataframe["return_24h"] < -float(self.buy_reentry_dump24.value))
            | (dataframe["return_72h"] < -float(self.buy_reentry_dump72.value))
            | dataframe["big_dump_candle"]
        )
        dataframe["panic_recent"] = dataframe["panic_seed"].rolling(panic_recent_window).max() > 0
        dataframe["low_zone_recent"] = (
            dataframe["panic_low_recent"] <= dataframe["rolling_low_30d"] * float(self.buy_reentry_low_buffer.value)
        ) | (
            dataframe["panic_low_recent"] <= dataframe["rolling_low_7d"] * float(self.buy_reentry_low_buffer.value)
        )
        dataframe["capitulation_recent"] = (
            (
                (dataframe["volume_ratio"] > float(self.buy_reentry_volume_ratio.value))
                | dataframe["atr_spike"]
                | (dataframe["lower_wick_ratio"] > 0.45)
            )
            .rolling(cap_recent_window)
            .max()
            > 0
        )

        if self.dp:
            if self.timeframe == "4h":
                dataframe["close_4h"] = dataframe["close"]
                for period in (20, 50, 100, 200):
                    dataframe[f"p4h_ema{period}_4h"] = ta.EMA(dataframe, timeperiod=period)
                dataframe["p4h_rsi_4h"] = ta.RSI(dataframe, timeperiod=14)
                dataframe["p4h_adx_4h"] = ta.ADX(dataframe, timeperiod=14)
                dataframe["p4h_ema50_slope_4h"] = dataframe["p4h_ema50_4h"] - dataframe["p4h_ema50_4h"].shift(4)
                dataframe["p4h_ema50_slope_pct_4h"] = self._safe_div(
                    dataframe["p4h_ema50_4h"], dataframe["p4h_ema50_4h"].shift(4)
                ) - 1.0
                dataframe["p4h_change3_4h"] = dataframe["close"] / dataframe["close"].shift(3) - 1
            else:
                pair_4h = self.dp.get_pair_dataframe(pair=metadata["pair"], timeframe="4h")
                for period in (20, 50, 100, 200):
                    pair_4h[f"p4h_ema{period}"] = ta.EMA(pair_4h, timeperiod=period)
                pair_4h["p4h_rsi"] = ta.RSI(pair_4h, timeperiod=14)
                pair_4h["p4h_adx"] = ta.ADX(pair_4h, timeperiod=14)
                pair_4h["p4h_ema50_slope"] = pair_4h["p4h_ema50"] - pair_4h["p4h_ema50"].shift(4)
                pair_4h["p4h_ema50_slope_pct"] = self._safe_div(pair_4h["p4h_ema50"], pair_4h["p4h_ema50"].shift(4)) - 1.0
                pair_4h["p4h_change3"] = pair_4h["close"] / pair_4h["close"].shift(3) - 1
                dataframe = merge_informative_pair(dataframe, pair_4h, self.timeframe, "4h", ffill=True)

            btc_1d = self.dp.get_pair_dataframe(pair="BTC/USDT", timeframe="1d")
            btc_1d["btc_ema50"] = ta.EMA(btc_1d, timeperiod=50)
            btc_1d["btc_ema100"] = ta.EMA(btc_1d, timeperiod=100)
            btc_1d["btc_ema200"] = ta.EMA(btc_1d, timeperiod=200)
            btc_1d["btc_rsi"] = ta.RSI(btc_1d, timeperiod=14)
            btc_1d["btc_ema50_slope"] = btc_1d["btc_ema50"] - btc_1d["btc_ema50"].shift(3)
            btc_1d["btc_ema50_slope_pct"] = self._safe_div(btc_1d["btc_ema50"], btc_1d["btc_ema50"].shift(3)) - 1.0
            btc_1d["btc_change3"] = btc_1d["close"] / btc_1d["close"].shift(3) - 1
            dataframe = merge_informative_pair(dataframe, btc_1d, self.timeframe, "1d", ffill=True)

        for column in dataframe.columns:
            if column != "date":
                try:
                    dataframe[column] = dataframe[column].ffill().fillna(0)
                except AttributeError:
                    pass
        ema200 = dataframe["btc_ema200_1d"]
        ema100 = dataframe["btc_ema100_1d"]
        ema50 = dataframe["btc_ema50_1d"]
        dataframe["btc_slow_reference_1d"] = ema200.where(
            ema200.gt(0), ema100.where(ema100.gt(0), ema50)
        )
        return dataframe

    def _v117_populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        market_core = (
            (dataframe["close_1d"] > dataframe["btc_ema50_1d"] * float(self.buy_btc_ema50_mult.value))
            & (dataframe["btc_ema50_1d"] > dataframe["btc_slow_reference_1d"] * float(self.buy_btc_ema200_mult.value))
            & (dataframe["btc_ema50_slope_pct_1d"] > float(self.buy_btc_slope_floor.value))
            & (dataframe["btc_rsi_1d"] > int(self.buy_btc_rsi_min.value))
        )
        market_extension = (
            (dataframe["close_1d"] > dataframe["btc_slow_reference_1d"] * float(self.buy_btc_extension_mult.value))
            & (dataframe["btc_ema50_slope_pct_1d"] > float(self.buy_btc_slope_floor.value))
            & (dataframe["btc_rsi_1d"] > int(self.buy_btc_extension_rsi.value))
        )
        market_ok = (market_core | market_extension) & (dataframe["btc_change3_1d"] > -float(self.buy_btc_change3_floor.value))

        trend_4h = (
            (dataframe["close_4h"] > dataframe["p4h_ema200_4h"] * float(self.buy_p4h_ema200_mult.value))
            & (dataframe["p4h_ema50_4h"] > dataframe["p4h_ema100_4h"])
            & (dataframe["p4h_ema100_4h"] > dataframe["p4h_ema200_4h"] * float(self.buy_p4h_ema100_mult.value))
            & (dataframe["p4h_ema50_slope_pct_4h"] > float(self.buy_p4h_slope_floor.value))
            & (dataframe["p4h_rsi_4h"] > int(self.buy_p4h_rsi_min.value))
            & (dataframe["p4h_adx_4h"] > int(self.buy_p4h_adx_min.value))
            & (dataframe["p4h_change3_4h"] > -float(self.buy_btc_change3_floor.value))
        )

        local_ok = (
            (dataframe["close"] > dataframe["ema200"] * float(self.buy_local_ema200_mult.value))
            & (dataframe["ema50"] > dataframe["ema100"] * 0.990)
            & (dataframe["ema50_slope_pct"] > float(self.buy_local_slope_floor.value))
            & (dataframe["rsi"] > int(self.buy_local_rsi_min.value))
            & (dataframe["rsi"] < int(self.buy_local_rsi_max.value))
            & (dataframe["adx"] > int(self.buy_local_adx_min.value))
            & (dataframe["atr_pct"] > float(self.buy_atr_min.value))
            & (dataframe["atr_pct"] < float(self.buy_atr_max.value))
            & (dataframe["volume"] > 0)
        )

        pullback = (
            (dataframe["low5"] <= dataframe["ema20"] * float(self.buy_pullback_ema_buffer.value))
            & (dataframe["close"] > dataframe["ema20"])
            & (dataframe["ema20"] > dataframe["ema50"])
            & (dataframe["close"] > dataframe["open"])
            & (dataframe["rsi"] > dataframe["rsi"].shift(1))
            & (dataframe["macdhist"] > dataframe["macdhist"].shift(1))
            & (dataframe["volume_ratio"] > float(self.buy_pullback_volume_min.value))
        )

        medium_regime = (
            (dataframe["p4h_adx_4h"] > int(self.buy_p4h_adx_min.value) + 7)
            & (dataframe["p4h_rsi_4h"] > int(self.buy_p4h_rsi_min.value) + 4)
            & (dataframe["adx"] > int(self.buy_local_adx_min.value) + 4)
            & (dataframe["btc_rsi_1d"] > int(self.buy_btc_rsi_min.value) + 3)
            & (dataframe["close_1d"] > dataframe["btc_ema50_1d"] * 1.010)
        )
        strong_regime = (
            (dataframe["p4h_adx_4h"] > int(self.buy_p4h_adx_min.value) + 10)
            & (dataframe["p4h_rsi_4h"] > int(self.buy_p4h_rsi_min.value) + 6)
            & (dataframe["adx"] > int(self.buy_local_adx_min.value) + 7)
            & (dataframe["btc_rsi_1d"] > int(self.buy_btc_rsi_min.value) + 5)
            & (dataframe["close_1d"] > dataframe["btc_ema50_1d"] * 1.020)
        )
        opportunistic_regime = (
            (dataframe["p4h_ema100_4h"] > dataframe["p4h_ema200_4h"] * float(self.buy_p4h_ema100_mult.value))
            & (dataframe["btc_ema50_1d"] > dataframe["btc_slow_reference_1d"] * 1.020)
            & (dataframe["p4h_rsi_4h"] > int(self.buy_p4h_rsi_min.value) + 4)
            & (dataframe["volume_ratio"] > 1.0)
            & (dataframe["retracement96"] < float(self.buy_retracement_max.value))
            & (dataframe["body_ratio"] > float(self.buy_body_ratio_min.value))
        )
        macro_overheat_guard = (
            bool(self.buy_macro_overheat_guard_enabled.value)
            & (dataframe["btc_rsi_1d"] > int(self.buy_macro_overheat_btc_rsi.value))
            & (dataframe["btc_change3_1d"] > float(self.buy_macro_overheat_btc_change3.value))
            & (dataframe["p4h_rsi_4h"] > int(self.buy_macro_overheat_p4h_rsi.value))
            & (dataframe["p4h_change3_4h"] > float(self.buy_macro_overheat_p4h_change3.value))
            & (dataframe["close_location"] < float(self.buy_macro_overheat_close_location.value))
        )

        trend_base = market_ok & trend_4h & local_ok & ~macro_overheat_guard
        dataframe.loc[bool(self.buy_opportunistic_enabled.value) & trend_base & pullback & opportunistic_regime, ["enter_long", "enter_tag"]] = (
            1,
            "trend_opportunistic_pullback",
        )
        dataframe.loc[bool(self.buy_medium_pullback_enabled.value) & trend_base & pullback & medium_regime, ["enter_long", "enter_tag"]] = (
            1,
            "trend_medium_pullback",
        )
        dataframe.loc[trend_base & pullback & strong_regime, ["enter_long", "enter_tag"]] = (
            1,
            "trend_strong_pullback",
        )

        breakout_high = dataframe[f"high{int(self.buy_breakout_window.value)}"]
        breakout_route = (
            bool(self.buy_breakout_enabled.value)
            & trend_base
            & (dataframe["close"] > breakout_high)
            & (dataframe["ema20"] > dataframe["ema50"])
            & (dataframe["ema50"] > dataframe["ema100"] * 0.990)
            & (dataframe["volume_ratio"] > float(self.buy_breakout_volume_min.value))
            & (dataframe["rsi"] > int(self.buy_breakout_rsi_min.value))
            & (dataframe["rsi"] < int(self.buy_breakout_rsi_max.value))
            & dataframe["macdhist_rising_2"]
            & (dataframe["close_location"] > float(self.buy_breakout_close_location.value))
            & (dataframe["volume"] > 0)
        )
        dataframe.loc[breakout_route, ["enter_long", "enter_tag"]] = (1, "bull_continuation_breakout")

        structured_entry = (
            (trend_base & pullback & (opportunistic_regime | (bool(self.buy_medium_pullback_enabled.value) & medium_regime) | strong_regime))
            | breakout_route
        )
        macro_bull_hold = (
            bool(self.buy_macro_enabled.value)
            & market_ok
            & trend_4h
            & (dataframe["close"] > dataframe["ema50"])
            & (dataframe["ema20"] > dataframe["ema50"])
            & (dataframe["p4h_ema50_4h"] > dataframe["p4h_ema100_4h"])
            & (dataframe["p4h_rsi_4h"] > int(self.buy_macro_rsi_min.value))
            & (dataframe["p4h_rsi_4h"] < int(self.buy_macro_rsi_max.value))
            & (dataframe["p4h_adx_4h"] > int(self.buy_macro_adx_min.value))
            & (dataframe["rsi"] > int(self.buy_local_rsi_min.value))
            & (dataframe["volume_ratio"] > float(self.buy_macro_volume_min.value))
            & (dataframe["volume"] > 0)
            & ~structured_entry
            & ~macro_overheat_guard
        )
        dataframe.loc[macro_bull_hold, ["enter_long", "enter_tag"]] = (1, "macro_bull_hold")

        btc_not_deep_crash = (
            (dataframe["btc_change3_1d"] > -float(self.buy_reentry_btc_change3_floor.value))
            & (dataframe["close_1d"] > dataframe["btc_slow_reference_1d"] * float(self.buy_reentry_btc_ema200_floor.value))
        )
        reclaim = (
            (
                (dataframe["close"] > dataframe["ema20"])
                & (dataframe["ema8"] > dataframe["ema20"])
                & (dataframe["close"] > dataframe["ema50"] * 0.985)
                & ((dataframe["close"].shift(1) <= dataframe["ema20"].shift(1)) | (dataframe["close"] > dataframe["range_high_12"]))
            )
            | ((dataframe["close"] > dataframe["ema50"]) & (dataframe["rsi"] > 45))
        )
        recovery_momentum = (
            ((dataframe["rsi7"] > int(self.buy_reentry_rsi7.value)) | (dataframe["rsi"] > int(self.buy_reentry_rsi14.value)))
            & dataframe["macdhist_rising_2"]
            & (dataframe["close"] > dataframe["open"])
            & (dataframe["close"] > dataframe["close"].shift(1))
            & (dataframe["close_location"] > 0.58)
            & (dataframe["volume_ratio"] > float(self.buy_reentry_volume_ratio.value))
        )
        rebound_btc_guard = (
            (dataframe["btc_change3_1d"] > -float(self.buy_rebound_btc_change3_floor.value))
            & (dataframe["close_1d"] > dataframe["btc_slow_reference_1d"] * float(self.buy_rebound_btc_ema200_floor.value))
        )
        rebound_setup = (
            dataframe["panic_recent"]
            | (dataframe["low_zone_recent"] & (dataframe["close"] > dataframe["range_high_12"]))
            | (
                (dataframe["close"] <= dataframe["rolling_low_30d"] * float(self.buy_rebound_max_from_low.value))
                & (dataframe["close"] > dataframe["range_high_12"])
            )
        )
        macro_bear_rebound = (
            bool(self.buy_rebound_enabled.value)
            & ~market_ok
            & rebound_setup
            & dataframe["capitulation_recent"]
            & reclaim
            & rebound_btc_guard
            & (dataframe["close"] <= dataframe["panic_low_recent"] * float(self.buy_rebound_max_from_low.value))
            & (dataframe["close_4h"] > dataframe["p4h_ema20_4h"] * 0.985)
            & (dataframe["p4h_rsi_4h"] > int(self.buy_rebound_p4h_rsi_min.value))
            & (dataframe["p4h_change3_4h"] > float(self.buy_rebound_p4h_change3_min.value))
            & (dataframe["close"] > dataframe["ema20"])
            & (dataframe["close"] > dataframe["ema50"] * 0.990)
            & ((dataframe["ema8"] > dataframe["ema20"]) | (dataframe["close"] > dataframe["range_high_12"]))
            & (dataframe["rsi"] > int(self.buy_rebound_rsi_min.value))
            & (dataframe["rsi"] < int(self.buy_rebound_rsi_max.value))
            & (dataframe["macdhist"] > dataframe["macdhist"].shift(1))
            & (dataframe["close"] > dataframe["close"].shift(1))
            & (dataframe["close_location"] > float(self.buy_rebound_close_location.value))
            & (dataframe["volume_ratio"] > float(self.buy_rebound_volume_min.value))
            & (dataframe["volume"] > 0)
            & ~structured_entry
            & ~macro_bull_hold
        )
        rebound_full_quality = (
            (dataframe["btc_ema50_slope_pct_1d"] > float(self.buy_rebound_full_btc_slope_min.value))
            & (dataframe["btc_rsi_1d"] > int(self.buy_rebound_full_btc_rsi_min.value))
            & (dataframe["p4h_ema50_slope_pct_4h"] > float(self.buy_rebound_full_p4h_slope_min.value))
        )
        macro_bear_rebound_full = macro_bear_rebound & rebound_full_quality
        macro_bear_rebound_weak = macro_bear_rebound & ~rebound_full_quality
        if bool(self.buy_rebound_weak_enabled.value):
            dataframe.loc[macro_bear_rebound_weak, ["enter_long", "enter_tag"]] = (1, "macro_bear_rebound_weak")
        dataframe.loc[macro_bear_rebound_full, ["enter_long", "enter_tag"]] = (1, "macro_bear_rebound")

        post_capitulation_reclaim = (
            bool(self.buy_reentry_enabled.value)
            & dataframe["panic_recent"]
            & dataframe["low_zone_recent"]
            & dataframe["capitulation_recent"]
            & reclaim
            & recovery_momentum
            & (dataframe["close"] <= dataframe["panic_low_recent"] * float(self.buy_reentry_max_from_low.value))
            & ((dataframe["close_4h"] > dataframe["p4h_ema20_4h"] * 0.980) & (dataframe["p4h_change3_4h"] > -0.060))
            & btc_not_deep_crash
            & (dataframe["volume"] > 0)
            & ~macro_bear_rebound
        )
        reentry_full_quality = (
            (dataframe["btc_ema50_slope_pct_1d"] > float(self.buy_reentry_full_btc_slope_min.value))
            & (dataframe["p4h_ema50_slope_pct_4h"] > float(self.buy_reentry_full_p4h_slope_min.value))
        )
        post_capitulation_reclaim_full = post_capitulation_reclaim & reentry_full_quality
        post_capitulation_reclaim_weak = post_capitulation_reclaim & ~reentry_full_quality
        if bool(self.buy_reentry_weak_enabled.value):
            dataframe.loc[post_capitulation_reclaim_weak, ["enter_long", "enter_tag"]] = (1, "post_capitulation_reclaim_weak")
        dataframe.loc[post_capitulation_reclaim_full, ["enter_long", "enter_tag"]] = (1, "post_capitulation_reclaim")

        bottom_btc_guard = (
            (dataframe["btc_change3_1d"] > -float(self.buy_bottom_btc_change3_floor.value))
            & (dataframe["close_1d"] > dataframe["btc_slow_reference_1d"] * float(self.buy_bottom_btc_ema200_floor.value))
        )
        bottom_reclaim = (
            bool(self.buy_bottom_enabled.value)
            & dataframe["bottom_seed_recent"]
            & ~structured_entry
            & ~post_capitulation_reclaim
            & (dataframe["close"] <= dataframe["panic_low_recent"] * float(self.buy_bottom_max_from_low.value))
            & (dataframe["close"] > dataframe["ema8"])
            & ((dataframe["close"] > dataframe["ema20"]) | (dataframe["close"] > dataframe["range_high_12"]))
            & (dataframe["rsi7"] > int(self.buy_bottom_reclaim_rsi7.value))
            & (dataframe["rsi7"] > dataframe["rsi7"].shift(1))
            & (dataframe["macdhist"] > dataframe["macdhist"].shift(1))
            & (
                (dataframe["close"] > dataframe["open"])
                | (dataframe["lower_wick_ratio"] > float(self.buy_bottom_pinbar_ratio.value))
            )
            & (dataframe["close_location"] > 0.45)
            & (dataframe["volume_ratio"] > 0.45)
            & bottom_btc_guard
            & (dataframe["volume"] > 0)
        )
        dataframe.loc[bottom_reclaim, ["enter_long", "enter_tag"]] = (1, "bottom_reclaim")
        return dataframe

    def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        macro_support = (
            (dataframe["btc_change3_1d"] > -float(self.sell_macro_btc_change3_floor.value))
            & (dataframe["close_1d"] > dataframe["btc_slow_reference_1d"] * float(self.sell_macro_btc_ema200_mult.value))
            & (dataframe["close_4h"] > dataframe["p4h_ema100_4h"] * float(self.sell_macro_p4h_ema100_mult.value))
            & (dataframe["p4h_ema50_4h"] > dataframe["p4h_ema100_4h"] * float(self.sell_macro_p4h_ema50_ema100_mult.value))
            & (dataframe["p4h_ema50_slope_4h"] > float(self.sell_macro_p4h_slope_floor.value))
        )
        soft_break = (
            (
                (dataframe["close_4h"] < dataframe["p4h_ema50_4h"] * float(self.sell_soft_p4h_ema50_mult.value))
                & (dataframe["p4h_ema50_slope_4h"] < float(self.sell_soft_p4h_slope_max.value))
            )
            | (
                (dataframe["close"] < dataframe["ema100"] * float(self.sell_soft_ema100_mult.value))
                & (dataframe["rsi"] < int(self.sell_soft_rsi_max.value))
                & (dataframe["macd"] < dataframe["macdsignal"])
            )
        )
        hard_break = (
            (dataframe["btc_change3_1d"] < -float(self.sell_hard_btc_change3.value))
            | (dataframe["close_4h"] < dataframe["p4h_ema200_4h"] * float(self.sell_hard_p4h_ema200_mult.value))
            | ((dataframe["close"] < dataframe["ema200"]) & (dataframe["rsi"] < int(self.sell_hard_rsi_max.value)))
        )
        rebound_exit_guard = (
            bool(self.buy_rebound_enabled.value)
            & (dataframe["btc_change3_1d"] > -float(self.buy_rebound_btc_change3_floor.value))
            & (dataframe["close_1d"] > dataframe["btc_slow_reference_1d"] * float(self.buy_rebound_btc_ema200_floor.value))
            & (dataframe["close"] <= dataframe["rolling_low_30d"] * float(self.buy_rebound_max_from_low.value))
            & (dataframe["close"] > dataframe["ema20"])
            & (dataframe["close"] > dataframe["ema50"] * 0.990)
            & ((dataframe["ema8"] > dataframe["ema20"]) | (dataframe["close"] > dataframe["range_high_12"]))
            & (dataframe["p4h_rsi_4h"] > int(self.buy_rebound_p4h_rsi_min.value))
            & (dataframe["p4h_change3_4h"] > float(self.buy_rebound_p4h_change3_min.value))
            & (dataframe["rsi"] > int(self.buy_rebound_rsi_min.value))
            & (dataframe["rsi"] < int(self.buy_rebound_rsi_max.value))
            & (dataframe["volume_ratio"] > float(self.buy_rebound_volume_min.value))
            & (dataframe["volume"] > 0)
        )
        trend_break = ((soft_break & ~macro_support) | hard_break) & (dataframe["volume"] > 0) & ~rebound_exit_guard
        dataframe.loc[trend_break, ["exit_long", "exit_tag"]] = (1, "v79_trend_break")
        return dataframe


    """V117 plus controlled weak-rebound scouting.

    V117 preserved drawdown by requiring full macro quality after a panic
    reclaim. That made it miss valid V-shaped rebounds while BTC/4h slope was
    still repairing. V118 keeps the V117 core, but allows small scout entries
    on weak rebound/reentry tags and exits them faster if the reclaim fails.
    """

    INTERFACE_VERSION = 3

    buy_scout_enabled = BooleanParameter(default=True, space="buy", optimize=False)
    buy_scout_btc_rsi_min = IntParameter(30, 50, default=35, space="buy", optimize=False)
    buy_scout_btc_change3_min = DecimalParameter(-0.010, 0.030, default=0.005, decimals=3, space="buy", optimize=False)
    buy_scout_btc_slope_min = DecimalParameter(-0.020, 0.005, default=-0.015, decimals=3, space="buy", optimize=False)
    buy_scout_p4h_slope_min = DecimalParameter(-0.002, 0.004, default=0.001, decimals=3, space="buy", optimize=False)
    buy_scout_volume_min = DecimalParameter(1.10, 2.60, default=1.5, decimals=2, space="buy", optimize=False)
    buy_scout_close_location = DecimalParameter(0.55, 0.85, default=0.65, decimals=2, space="buy", optimize=False)
    buy_scout_rsi_max = IntParameter(74, 86, default=82, space="buy", optimize=False)
    buy_reentry_confirm_guard_enabled = BooleanParameter(default=True, space="buy", optimize=False)
    buy_reentry_confirm_p4h_change_min = DecimalParameter(-0.005, 0.020, default=0.0, decimals=3, space="buy", optimize=False)
    buy_reentry_confirm_p4h_rsi_min = IntParameter(52, 64, default=58, space="buy", optimize=False)
    buy_reentry_confirm_p4h_strong_change = DecimalParameter(0.005, 0.040, default=0.018, decimals=3, space="buy", optimize=False)

    def _v118_custom_stake_amount(
        self,
        pair: str,
        current_time: datetime,
        current_rate: float,
        proposed_stake: float,
        min_stake: float | None,
        max_stake: float,
        leverage: float,
        entry_tag: str | None,
        side: str,
        **kwargs: Any,
    ) -> float:
        if entry_tag == "macro_bear_rebound_scout":
            stake = proposed_stake * float(self.buy_rebound_weak_stake_pct.value)
            stake = min(float(stake), float(max_stake))
            if min_stake is not None and stake > 0:
                stake = max(stake, float(min_stake))
            return stake
        return self._v117_custom_stake_amount(
            pair,
            current_time,
            current_rate,
            proposed_stake,
            min_stake,
            max_stake,
            leverage,
            entry_tag,
            side,
            **kwargs,
        )

    def _v118_custom_stoploss(
        self,
        pair: str,
        trade: Trade,
        current_time: datetime,
        current_rate: float,
        current_profit: float,
        after_fill: bool,
        **kwargs: Any,
    ) -> float | None:
        if trade.enter_tag == "macro_bear_rebound_scout":
            if current_profit > 0.100:
                return 0.030
            if current_profit > 0.050:
                return 0.010
            try:
                dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
                candle = dataframe.iloc[-1]
                atr = float(candle.get("atr", trade.open_rate * 0.025))
                recent_low = float(candle.get("panic_low_recent", trade.open_rate * 0.90))
                stop_price = max(recent_low - 0.35 * atr, trade.open_rate * 0.925)
                stop_price = min(stop_price, trade.open_rate * 0.985)
                return stoploss_from_absolute(stop_price, current_rate, is_short=False, leverage=1.0)
            except (AttributeError, IndexError, KeyError, TypeError, ValueError):
                return 0.075
        return self._v117_custom_stoploss(pair, trade, current_time, current_rate, current_profit, after_fill, **kwargs)

    def _v118_custom_exit(
        self,
        pair: str,
        trade: Trade,
        current_time: datetime,
        current_rate: float,
        current_profit: float,
        **kwargs: Any,
    ) -> str | bool | None:
        parent_exit = self._v117_custom_exit(pair, trade, current_time, current_rate, current_profit, **kwargs)
        if parent_exit:
            return parent_exit

        if trade.enter_tag not in {"macro_bear_rebound_weak", "post_capitulation_reclaim_weak", "macro_bear_rebound_scout"}:
            return None

        try:
            dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
            candle = dataframe.iloc[-1]
            close = float(candle.get("close", current_rate))
            ema20 = float(candle.get("ema20", current_rate))
            ema50 = float(candle.get("ema50", current_rate))
            macd = float(candle.get("macd", 0.0))
            macdsignal = float(candle.get("macdsignal", 0.0))
            rsi = float(candle.get("rsi", 50.0))
            p4h_change3 = float(candle.get("p4h_change3_4h", 0.0))
            btc_change3 = float(candle.get("btc_change3_1d", 0.0))
        except (AttributeError, IndexError, KeyError, TypeError, ValueError):
            return None

        age_hours = (current_time - trade.open_date_utc).total_seconds() / 3600.0
        reclaim_failed = close < ema20 and macd < macdsignal
        weak_macro_rollover = btc_change3 < -0.045 or p4h_change3 < -0.035

        if current_profit < -0.030 and (reclaim_failed or weak_macro_rollover):
            return "weak_rebound_failed"
        if age_hours >= 24 and current_profit < 0.010 and close < ema50 and rsi < 52:
            return "weak_rebound_time_stop"
        if current_profit > 0.075 and close < ema20 and macd < macdsignal:
            return "weak_rebound_profit_guard"
        return None

    def _v118_populate_entry_trend(self, dataframe, metadata: dict):
        dataframe = self._v117_populate_entry_trend(dataframe, metadata)

        if bool(self.buy_reentry_confirm_guard_enabled.value):
            post_reclaim_tag = dataframe["enter_tag"].eq("post_capitulation_reclaim")
            reentry_confirmed = (
                (dataframe["p4h_change3_4h"] > float(self.buy_reentry_confirm_p4h_change_min.value))
                & (
                    (dataframe["close"] > dataframe["range_high_12"])
                    | (
                        (dataframe["p4h_rsi_4h"] > int(self.buy_reentry_confirm_p4h_rsi_min.value))
                        & (
                            dataframe["p4h_change3_4h"]
                            > float(self.buy_reentry_confirm_p4h_strong_change.value)
                        )
                    )
                )
            )
            dataframe.loc[post_reclaim_tag & ~reentry_confirmed, ["enter_long", "enter_tag"]] = (0, "")

        market_core = (
            (dataframe["close_1d"] > dataframe["btc_ema50_1d"] * float(self.buy_btc_ema50_mult.value))
            & (dataframe["btc_ema50_1d"] > dataframe["btc_slow_reference_1d"] * float(self.buy_btc_ema200_mult.value))
            & (dataframe["btc_ema50_slope_pct_1d"] > float(self.buy_btc_slope_floor.value))
            & (dataframe["btc_rsi_1d"] > int(self.buy_btc_rsi_min.value))
        )
        market_extension = (
            (dataframe["close_1d"] > dataframe["btc_slow_reference_1d"] * float(self.buy_btc_extension_mult.value))
            & (dataframe["btc_ema50_slope_pct_1d"] > float(self.buy_btc_slope_floor.value))
            & (dataframe["btc_rsi_1d"] > int(self.buy_btc_extension_rsi.value))
        )
        market_ok = (market_core | market_extension) & (
            dataframe["btc_change3_1d"] > -float(self.buy_btc_change3_floor.value)
        )

        rebound_setup = (
            dataframe["panic_recent"]
            | (dataframe["low_zone_recent"] & (dataframe["close"] > dataframe["range_high_12"]))
            | (
                (dataframe["close"] <= dataframe["rolling_low_30d"] * float(self.buy_rebound_max_from_low.value))
                & (dataframe["close"] > dataframe["range_high_12"])
            )
        )
        reclaim = (
            (
                (dataframe["close"] > dataframe["ema20"])
                & (dataframe["ema8"] > dataframe["ema20"])
                & (dataframe["close"] > dataframe["ema50"] * 0.985)
                & (
                    (dataframe["close"].shift(1) <= dataframe["ema20"].shift(1))
                    | (dataframe["close"] > dataframe["range_high_12"])
                )
            )
            | ((dataframe["close"] > dataframe["ema50"]) & (dataframe["rsi"] > 45))
        )
        scout_rebound = (
            bool(self.buy_scout_enabled.value)
            & ~market_ok
            & rebound_setup
            & dataframe["capitulation_recent"]
            & reclaim
            & (dataframe["btc_change3_1d"] > -float(self.buy_rebound_btc_change3_floor.value))
            & (dataframe["btc_change3_1d"] > float(self.buy_scout_btc_change3_min.value))
            & (dataframe["close_1d"] > dataframe["btc_slow_reference_1d"] * float(self.buy_rebound_btc_ema200_floor.value))
            & (dataframe["btc_rsi_1d"] > int(self.buy_scout_btc_rsi_min.value))
            & (dataframe["btc_ema50_slope_pct_1d"] > float(self.buy_scout_btc_slope_min.value))
            & (dataframe["close"] <= dataframe["panic_low_recent"] * float(self.buy_rebound_max_from_low.value))
            & (dataframe["close_4h"] > dataframe["p4h_ema20_4h"] * 0.985)
            & (dataframe["p4h_rsi_4h"] > int(self.buy_rebound_p4h_rsi_min.value))
            & (dataframe["p4h_change3_4h"] > float(self.buy_rebound_p4h_change3_min.value))
            & (dataframe["p4h_ema50_slope_pct_4h"] > float(self.buy_scout_p4h_slope_min.value))
            & (dataframe["close"] > dataframe["ema20"])
            & (dataframe["close"] > dataframe["ema50"] * 0.990)
            & ((dataframe["ema8"] > dataframe["ema20"]) | (dataframe["close"] > dataframe["range_high_12"]))
            & (dataframe["rsi"] > int(self.buy_rebound_rsi_min.value))
            & (dataframe["rsi"] < int(self.buy_scout_rsi_max.value))
            & (dataframe["macdhist"] > dataframe["macdhist"].shift(1))
            & (dataframe["close"] > dataframe["close"].shift(1))
            & (dataframe["close_location"] > float(self.buy_scout_close_location.value))
            & (dataframe["volume_ratio"] > float(self.buy_scout_volume_min.value))
            & (dataframe["volume"] > 0)
            & (dataframe.get("enter_long", 0) != 1)
        )
        dataframe.loc[scout_rebound, ["enter_long", "enter_tag"]] = (1, "macro_bear_rebound_scout")
        return dataframe


    """V118 entries with dynamic stake sizing.

    This keeps V118's signal engine intact and changes only capital allocation.
    The goal is to reduce exposure during weak macro / high-volatility moments,
    while allowing near-full stake on the strongest V118 setups.
    """

    INTERFACE_VERSION = 3

    rm_enabled = BooleanParameter(default=True, space="buy", optimize=True)
    rm_base_stake_cap = DecimalParameter(0.55, 1.00, default=1.0, decimals=2, space="buy", optimize=True)
    rm_min_stake_mult = DecimalParameter(0.20, 1.00, default=0.9, decimals=2, space="buy", optimize=True)

    rm_macro_mult = DecimalParameter(0.70, 1.05, default=1.0, decimals=2, space="buy", optimize=True)
    rm_trend_mult = DecimalParameter(0.80, 1.10, default=1.05, decimals=2, space="buy", optimize=True)
    rm_rebound_mult = DecimalParameter(0.55, 1.05, default=0.97, decimals=2, space="buy", optimize=True)
    rm_reentry_mult = DecimalParameter(0.50, 1.00, default=0.94, decimals=2, space="buy", optimize=True)
    rm_scout_mult = DecimalParameter(0.55, 1.10, default=1.0, decimals=2, space="buy", optimize=True)

    rm_atr_soft = DecimalParameter(0.030, 0.105, default=0.085, decimals=3, space="buy", optimize=True)
    rm_atr_hard = DecimalParameter(0.050, 0.120, default=0.12, decimals=3, space="buy", optimize=True)
    rm_atr_soft_mult = DecimalParameter(0.55, 1.00, default=0.98, decimals=2, space="buy", optimize=True)
    rm_atr_hard_mult = DecimalParameter(0.30, 1.00, default=0.92, decimals=2, space="buy", optimize=True)

    rm_btc_weak_change3 = DecimalParameter(-0.080, 0.010, default=-0.08, decimals=3, space="buy", optimize=True)
    rm_btc_weak_mult = DecimalParameter(0.45, 0.95, default=0.95, decimals=2, space="buy", optimize=True)
    rm_p4h_weak_change3 = DecimalParameter(-0.060, 0.010, default=-0.06, decimals=3, space="buy", optimize=True)
    rm_p4h_weak_mult = DecimalParameter(0.45, 0.95, default=0.95, decimals=2, space="buy", optimize=True)

    rm_ema20_ext_soft = DecimalParameter(1.040, 1.200, default=1.16, decimals=3, space="buy", optimize=True)
    rm_ema20_ext_hard = DecimalParameter(1.080, 1.300, default=1.24, decimals=3, space="buy", optimize=True)
    rm_extension_soft_mult = DecimalParameter(0.55, 1.00, default=0.98, decimals=2, space="buy", optimize=True)
    rm_extension_hard_mult = DecimalParameter(0.30, 1.00, default=0.93, decimals=2, space="buy", optimize=True)
    rm_hot_rsi = IntParameter(72, 88, default=88, space="buy", optimize=True)
    rm_hot_rsi_mult = DecimalParameter(0.45, 0.95, default=0.95, decimals=2, space="buy", optimize=True)

    rm_volume_floor = DecimalParameter(0.30, 0.85, default=0.4, decimals=2, space="buy", optimize=True)
    rm_volume_floor_mult = DecimalParameter(0.55, 1.00, default=0.9, decimals=2, space="buy", optimize=True)

    rm_strong_boost = DecimalParameter(1.00, 1.18, default=1.08, decimals=2, space="buy", optimize=True)
    rm_strong_btc_change3 = DecimalParameter(0.010, 0.080, default=0.025, decimals=3, space="buy", optimize=True)
    rm_strong_p4h_change3 = DecimalParameter(0.008, 0.050, default=0.02, decimals=3, space="buy", optimize=True)
    rm_strong_p4h_rsi = IntParameter(58, 76, default=66, space="buy", optimize=True)

    rm_dd_start = DecimalParameter(0.060, 0.220, default=0.22, decimals=3, space="buy", optimize=True)
    rm_dd_hard = DecimalParameter(0.120, 0.320, default=0.3, decimals=3, space="buy", optimize=True)
    rm_dd_hard_mult = DecimalParameter(0.35, 0.90, default=0.9, decimals=2, space="buy", optimize=True)
    rm_recent_loss_mult = DecimalParameter(0.50, 0.95, default=0.94, decimals=2, space="buy", optimize=True)

    rm_early_exit_enabled = BooleanParameter(default=False, space="sell", optimize=True)
    rm_macro_fail_profit = DecimalParameter(-0.085, -0.025, default=-0.05, decimals=3, space="sell", optimize=True)
    rm_reclaim_fail_profit = DecimalParameter(-0.060, -0.012, default=-0.026, decimals=3, space="sell", optimize=True)
    rm_shock_profit = DecimalParameter(-0.060, -0.012, default=-0.025, decimals=3, space="sell", optimize=True)
    rm_profit_guard_level = DecimalParameter(0.030, 0.090, default=0.055, decimals=3, space="sell", optimize=True)
    rm_profit_guard_pullback = DecimalParameter(0.012, 0.045, default=0.025, decimals=3, space="sell", optimize=True)

    def _latest_candle(self, pair: str, current_time: datetime) -> dict[str, Any]:
        if not self.dp:
            return {}
        dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
        if dataframe is None or dataframe.empty:
            return {}
        if "date" in dataframe.columns:
            sliced = dataframe.loc[dataframe["date"] <= current_time]
            if not sliced.empty:
                return sliced.iloc[-1].to_dict()
        return dataframe.iloc[-1].to_dict()

    @staticmethod
    def _as_float(candle: dict[str, Any], key: str, default: float) -> float:
        try:
            value = float(candle.get(key, default))
            if value != value:
                return default
            return value
        except (TypeError, ValueError):
            return default

    def _wallet_dd_multiplier(self) -> float:
        try:
            total = float(self.wallets.get_total_stake_amount())
        except (AttributeError, TypeError, ValueError):
            return 1.0
        if total <= 0:
            return 1.0

        peak = float(getattr(self, "_rm_peak_balance", 0.0))
        peak = max(peak, total)
        setattr(self, "_rm_peak_balance", peak)

        drawdown = max(0.0, (peak - total) / max(peak, 1e-9))
        start = float(self.rm_dd_start.value)
        hard = float(self.rm_dd_hard.value)
        if drawdown <= start:
            return 1.0
        if drawdown >= hard:
            return float(self.rm_dd_hard_mult.value)

        span = max(hard - start, 1e-9)
        pressure = (drawdown - start) / span
        hard_mult = float(self.rm_dd_hard_mult.value)
        return 1.0 - pressure * (1.0 - hard_mult)

    def _recent_trade_multiplier(self, pair: str, current_time: datetime) -> float:
        try:
            trades = Trade.get_trades_proxy(pair=pair, is_open=False)
        except (AttributeError, TypeError):
            return 1.0

        closed: list[Trade] = []
        for trade in trades:
            close_date = getattr(trade, "close_date_utc", None)
            if close_date is not None and close_date < current_time:
                closed.append(trade)
        if len(closed) < 2:
            return 1.0

        closed.sort(key=lambda item: item.close_date_utc)
        recent = closed[-4:]
        losses = 0
        profit_sum = 0.0
        for trade in recent:
            profit_ratio = float(getattr(trade, "close_profit", 0.0) or 0.0)
            profit_sum += profit_ratio
            if profit_ratio < 0:
                losses += 1
        if losses >= 2 or profit_sum < -0.03:
            return float(self.rm_recent_loss_mult.value)
        return 1.0

    def _rm_tag_multiplier(self, entry_tag: str | None) -> float:
        tag = entry_tag or ""
        if tag == "macro_bull_hold":
            return float(self.rm_macro_mult.value)
        if tag in {"trend_strong_pullback", "trend_medium_pullback", "trend_opportunistic_pullback"}:
            return float(self.rm_trend_mult.value)
        if tag == "macro_bear_rebound":
            return float(self.rm_rebound_mult.value)
        if tag in {"post_capitulation_reclaim", "bottom_reclaim"}:
            return float(self.rm_reentry_mult.value)
        if tag in {"macro_bear_rebound_scout", "macro_bear_rebound_weak", "post_capitulation_reclaim_weak"}:
            return float(self.rm_scout_mult.value)
        return 1.0

    def _market_multiplier(self, candle: dict[str, Any]) -> float:
        close = self._as_float(candle, "close", 0.0)
        ema20 = self._as_float(candle, "ema20", close)
        rsi = self._as_float(candle, "rsi", 50.0)
        atr_pct = self._as_float(candle, "atr_pct", 0.0)
        volume_ratio = self._as_float(candle, "volume_ratio", 1.0)
        btc_change3 = self._as_float(candle, "btc_change3_1d", 0.0)
        p4h_change3 = self._as_float(candle, "p4h_change3_4h", 0.0)
        p4h_rsi = self._as_float(candle, "p4h_rsi_4h", 50.0)

        mult = float(self.rm_base_stake_cap.value)

        if atr_pct > float(self.rm_atr_hard.value):
            mult *= float(self.rm_atr_hard_mult.value)
        elif atr_pct > float(self.rm_atr_soft.value):
            mult *= float(self.rm_atr_soft_mult.value)

        if btc_change3 < float(self.rm_btc_weak_change3.value):
            mult *= float(self.rm_btc_weak_mult.value)
        if p4h_change3 < float(self.rm_p4h_weak_change3.value):
            mult *= float(self.rm_p4h_weak_mult.value)

        if close > 0 and ema20 > 0:
            extension = close / ema20
            if extension > float(self.rm_ema20_ext_hard.value):
                mult *= float(self.rm_extension_hard_mult.value)
            elif extension > float(self.rm_ema20_ext_soft.value):
                mult *= float(self.rm_extension_soft_mult.value)

        if rsi > int(self.rm_hot_rsi.value):
            mult *= float(self.rm_hot_rsi_mult.value)
        if volume_ratio < float(self.rm_volume_floor.value):
            mult *= float(self.rm_volume_floor_mult.value)

        strong = (
            btc_change3 > float(self.rm_strong_btc_change3.value)
            and p4h_change3 > float(self.rm_strong_p4h_change3.value)
            and p4h_rsi > int(self.rm_strong_p4h_rsi.value)
            and atr_pct <= float(self.rm_atr_soft.value)
        )
        if strong:
            mult *= float(self.rm_strong_boost.value)
        return mult

    def custom_stake_amount(
        self,
        pair: str,
        current_time: datetime,
        current_rate: float,
        proposed_stake: float,
        min_stake: float | None,
        max_stake: float,
        leverage: float,
        entry_tag: str | None,
        side: str,
        **kwargs: Any,
    ) -> float:
        base_stake = self._v118_custom_stake_amount(
            pair,
            current_time,
            current_rate,
            proposed_stake,
            min_stake,
            max_stake,
            leverage,
            entry_tag,
            side,
            **kwargs,
        )
        if not bool(self.rm_enabled.value):
            if entry_tag == "v122_late_reclaim":
                late_stake = min(
                    float(base_stake) * float(self.v122_late_stake_mult.value),
                    float(max_stake),
                )
                if min_stake is not None and late_stake > 0:
                    late_stake = max(late_stake, float(min_stake))
                return late_stake
            return base_stake

        candle = self._latest_candle(pair, current_time)
        multiplier = (
            self._tag_multiplier(entry_tag)
            * self._market_multiplier(candle)
            * self._wallet_dd_multiplier()
            * self._recent_trade_multiplier(pair, current_time)
        )
        multiplier = max(float(self.rm_min_stake_mult.value), min(multiplier, 1.10))
        stake = min(float(base_stake) * multiplier, float(max_stake))
        if min_stake is not None and stake > 0:
            stake = max(stake, float(min_stake))
        return stake

    def _rm_custom_exit(
        self,
        pair: str,
        trade: Trade,
        current_time: datetime,
        current_rate: float,
        current_profit: float,
        **kwargs: Any,
    ) -> str | bool | None:
        if bool(self.rm_early_exit_enabled.value):
            try:
                dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
                candle = dataframe.iloc[-1]
                prev = dataframe.iloc[-2] if len(dataframe) > 1 else candle
                close = float(candle.get("close", current_rate))
                ema20 = float(candle.get("ema20", current_rate))
                ema50 = float(candle.get("ema50", current_rate))
                rsi = float(candle.get("rsi", 50.0))
                macd = float(candle.get("macd", 0.0))
                macdsignal = float(candle.get("macdsignal", 0.0))
                macdhist = float(candle.get("macdhist", 0.0))
                prev_macdhist = float(prev.get("macdhist", macdhist))
                close_location = float(candle.get("close_location", 0.5))
                btc_change3 = float(candle.get("btc_change3_1d", 0.0))
                p4h_change3 = float(candle.get("p4h_change3_4h", 0.0))
            except (AttributeError, IndexError, KeyError, TypeError, ValueError):
                return self._v118_custom_exit(pair, trade, current_time, current_rate, current_profit, **kwargs)

            tag = trade.enter_tag or ""
            weak_intraday = close < ema20 and macd < macdsignal and macdhist < prev_macdhist
            weak_structure = close < ema50 and rsi < 52 and close_location < 0.55
            macro_shock = btc_change3 < -0.055 or p4h_change3 < -0.040
            reclaim_tag = tag in {"macro_bear_rebound", "macro_bear_rebound_scout", "post_capitulation_reclaim", "bottom_reclaim"}

            if current_profit < float(self.rm_shock_profit.value) and macro_shock and close < ema20:
                return "rm_market_shock_exit"
            if tag == "macro_bull_hold" and current_profit < float(self.rm_macro_fail_profit.value):
                if weak_intraday and weak_structure:
                    return "rm_macro_fail_exit"
            if reclaim_tag and current_profit < float(self.rm_reclaim_fail_profit.value):
                if weak_intraday or (weak_structure and p4h_change3 < -0.015):
                    return "rm_reclaim_fail_exit"
            if current_profit > float(self.rm_profit_guard_level.value):
                pullback = (trade.max_rate - current_rate) / max(trade.max_rate, 1e-9)
                if pullback > float(self.rm_profit_guard_pullback.value) and weak_intraday:
                    return "rm_profit_guard_exit"

        return self._v118_custom_exit(pair, trade, current_time, current_rate, current_profit, **kwargs)

    def leverage(
        self,
        pair: str,
        current_time: datetime,
        current_rate: float,
        proposed_leverage: float,
        max_leverage: float,
        entry_tag: str | None,
        side: str,
        **kwargs: Any,
    ) -> float:
        return 1.0


    """Risk-aware V118 research branch.

    V122 keeps V118's proven core intact, then adds two live-risk controls:
    light capital throttling for weak / stretched conditions, and a small
    late-reclaim entry so the bot can participate after a confirmed recovery
    without trying to catch the bottom.
    """

    INTERFACE_VERSION = 3

    rm_min_stake_mult = DecimalParameter(0.35, 0.98, default=0.9, decimals=2, space="buy", optimize=True)
    rm_rebound_mult = DecimalParameter(0.60, 1.05, default=0.97, decimals=2, space="buy", optimize=True)
    rm_reentry_mult = DecimalParameter(0.55, 1.05, default=0.94, decimals=2, space="buy", optimize=True)
    rm_scout_mult = DecimalParameter(0.60, 1.08, default=1.0, decimals=2, space="buy", optimize=True)

    rm_atr_soft = DecimalParameter(0.040, 0.110, default=0.085, decimals=3, space="buy", optimize=True)
    rm_atr_hard = DecimalParameter(0.060, 0.160, default=0.12, decimals=3, space="buy", optimize=True)
    rm_atr_soft_mult = DecimalParameter(0.65, 1.00, default=0.98, decimals=2, space="buy", optimize=True)
    rm_atr_hard_mult = DecimalParameter(0.45, 0.98, default=0.92, decimals=2, space="buy", optimize=True)

    rm_btc_weak_change3 = DecimalParameter(-0.120, -0.005, default=-0.08, decimals=3, space="buy", optimize=True)
    rm_btc_weak_mult = DecimalParameter(0.55, 1.00, default=0.95, decimals=2, space="buy", optimize=True)
    rm_p4h_weak_change3 = DecimalParameter(-0.100, -0.005, default=-0.06, decimals=3, space="buy", optimize=True)
    rm_p4h_weak_mult = DecimalParameter(0.55, 1.00, default=0.95, decimals=2, space="buy", optimize=True)

    rm_ema20_ext_soft = DecimalParameter(1.060, 1.200, default=1.16, decimals=3, space="buy", optimize=True)
    rm_ema20_ext_hard = DecimalParameter(1.110, 1.300, default=1.24, decimals=3, space="buy", optimize=True)
    rm_extension_soft_mult = DecimalParameter(0.65, 1.00, default=0.98, decimals=2, space="buy", optimize=True)
    rm_extension_hard_mult = DecimalParameter(0.45, 0.98, default=0.93, decimals=2, space="buy", optimize=True)
    rm_hot_rsi = IntParameter(76, 92, default=88, space="buy", optimize=True)
    rm_hot_rsi_mult = DecimalParameter(0.55, 1.00, default=0.95, decimals=2, space="buy", optimize=True)

    rm_dd_start = DecimalParameter(0.120, 0.300, default=0.22, decimals=3, space="buy", optimize=True)
    rm_dd_hard = DecimalParameter(0.180, 0.380, default=0.3, decimals=3, space="buy", optimize=True)
    rm_dd_hard_mult = DecimalParameter(0.50, 0.98, default=0.9, decimals=2, space="buy", optimize=True)
    rm_recent_loss_mult = DecimalParameter(0.60, 1.00, default=0.94, decimals=2, space="buy", optimize=True)

    v122_fee_safety_enabled = BooleanParameter(default=False, space="buy", optimize=True)
    v122_block_hot_rsi = IntParameter(78, 90, default=84, space="buy", optimize=True)
    v122_block_extension = DecimalParameter(1.080, 1.200, default=1.14, decimals=3, space="buy", optimize=True)
    v122_block_volume_floor = DecimalParameter(0.35, 1.10, default=0.7, decimals=2, space="buy", optimize=True)
    v122_block_close_location = DecimalParameter(0.35, 0.70, default=0.5, decimals=2, space="buy", optimize=True)

    v122_late_reclaim_enabled = BooleanParameter(default=False, space="buy", optimize=True)
    v122_late_stake_mult = DecimalParameter(0.10, 0.75, default=0.45, decimals=2, space="buy", optimize=True)
    v122_late_volume_min = DecimalParameter(0.80, 2.20, default=1.15, decimals=2, space="buy", optimize=True)
    v122_late_rsi_min = IntParameter(48, 64, default=54, space="buy", optimize=True)
    v122_late_rsi_max = IntParameter(66, 84, default=76, space="buy", optimize=True)
    v122_late_max_extension = DecimalParameter(1.030, 1.130, default=1.08, decimals=3, space="buy", optimize=True)
    v122_late_btc_change_floor = DecimalParameter(-0.090, -0.005, default=-0.045, decimals=3, space="buy", optimize=True)
    v122_late_p4h_change_floor = DecimalParameter(-0.040, 0.030, default=-0.005, decimals=3, space="buy", optimize=True)
    v122_late_close_location = DecimalParameter(0.48, 0.78, default=0.58, decimals=2, space="buy", optimize=True)

    def _tag_multiplier(self, entry_tag: str | None) -> float:
        if entry_tag == "v122_late_reclaim":
            return float(self.v122_late_stake_mult.value)
        return self._rm_tag_multiplier(entry_tag)

    def custom_stoploss(
        self,
        pair: str,
        trade: Trade,
        current_time: datetime,
        current_rate: float,
        current_profit: float,
        after_fill: bool,
        **kwargs: Any,
    ) -> float | None:
        if trade.enter_tag == "v122_late_reclaim":
            if current_profit > 0.080:
                return 0.020
            if current_profit > 0.040:
                return 0.005
            try:
                dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
                candle = dataframe.iloc[-1]
                atr = float(candle.get("atr", trade.open_rate * 0.025))
                ema50 = float(candle.get("ema50", trade.open_rate * 0.95))
                stop_price = max(min(ema50 - 0.6 * atr, trade.open_rate * 0.985), trade.open_rate * 0.925)
                return stoploss_from_absolute(stop_price, current_rate, is_short=False, leverage=1.0)
            except (AttributeError, IndexError, KeyError, TypeError, ValueError):
                return 0.075
        return self._v118_custom_stoploss(pair, trade, current_time, current_rate, current_profit, after_fill, **kwargs)

    def custom_exit(
        self,
        pair: str,
        trade: Trade,
        current_time: datetime,
        current_rate: float,
        current_profit: float,
        **kwargs: Any,
    ) -> str | bool | None:
        parent_exit = self._rm_custom_exit(pair, trade, current_time, current_rate, current_profit, **kwargs)
        if parent_exit:
            return parent_exit
        if trade.enter_tag != "v122_late_reclaim":
            return None

        try:
            dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
            candle = dataframe.iloc[-1]
            close = float(candle.get("close", current_rate))
            ema20 = float(candle.get("ema20", current_rate))
            ema50 = float(candle.get("ema50", current_rate))
            rsi = float(candle.get("rsi", 50.0))
            macd = float(candle.get("macd", 0.0))
            macdsignal = float(candle.get("macdsignal", 0.0))
            btc_change3 = float(candle.get("btc_change3_1d", 0.0))
            p4h_change3 = float(candle.get("p4h_change3_4h", 0.0))
        except (AttributeError, IndexError, KeyError, TypeError, ValueError):
            return None

        age_hours = (current_time - trade.open_date_utc).total_seconds() / 3600.0
        if current_profit < -0.028 and (close < ema50 or btc_change3 < -0.055 or p4h_change3 < -0.035):
            return "v122_late_failed"
        if age_hours >= 30 and current_profit < 0.008 and close < ema20 and rsi < 55:
            return "v122_late_time_stop"
        if current_profit > 0.055 and close < ema20 and macd < macdsignal:
            return "v122_late_profit_guard"
        return None

    def populate_entry_trend(self, dataframe, metadata: dict):
        dataframe = self._v118_populate_entry_trend(dataframe, metadata)

        if bool(self.v122_fee_safety_enabled.value):
            extension = dataframe["close"] / dataframe["ema20"].replace(0, float("nan"))
            hot_weak_close = (
                dataframe["enter_tag"].eq("macro_bull_hold")
                & (dataframe["rsi"] > int(self.v122_block_hot_rsi.value))
                & (extension > float(self.v122_block_extension.value))
                & (dataframe["volume_ratio"] < float(self.v122_block_volume_floor.value))
                & (dataframe["close_location"] < float(self.v122_block_close_location.value))
            )
            weak_reclaim_quality = (
                dataframe["enter_tag"].isin(["macro_bear_rebound_scout", "post_capitulation_reclaim"])
                & (
                    (dataframe["close_location"] < 0.48)
                    | (
                        (dataframe["volume_ratio"] < 0.75)
                        & (dataframe["p4h_change3_4h"] < -0.020)
                    )
                )
            )
            dataframe.loc[hot_weak_close | weak_reclaim_quality, ["enter_long", "enter_tag"]] = (0, "")

        extension = dataframe["close"] / dataframe["ema20"].replace(0, float("nan"))
        late_reclaim = (
            bool(self.v122_late_reclaim_enabled.value)
            & (dataframe.get("enter_long", 0) != 1)
            & (dataframe["btc_change3_1d"] > float(self.v122_late_btc_change_floor.value))
            & (dataframe["p4h_change3_4h"] > float(self.v122_late_p4h_change_floor.value))
            & (dataframe["close_1d"] > dataframe["btc_slow_reference_1d"] * 0.82)
            & (dataframe["close_4h"] > dataframe["p4h_ema20_4h"] * 0.985)
            & (dataframe["close"] > dataframe["ema20"])
            & (dataframe["close"] > dataframe["ema50"])
            & ((dataframe["ema8"] > dataframe["ema20"]) | (dataframe["close"] > dataframe["range_high_12"]))
            & (dataframe["close"] > dataframe["range_high_12"])
            & (dataframe["rsi"] > int(self.v122_late_rsi_min.value))
            & (dataframe["rsi"] < int(self.v122_late_rsi_max.value))
            & (dataframe["macdhist"] > dataframe["macdhist"].shift(1))
            & (dataframe["macdhist"].shift(1) > dataframe["macdhist"].shift(2))
            & (dataframe["volume_ratio"] > float(self.v122_late_volume_min.value))
            & (dataframe["close_location"] > float(self.v122_late_close_location.value))
            & (extension < float(self.v122_late_max_extension.value))
            & (dataframe["atr_pct"] < float(self.buy_atr_max.value))
            & (dataframe["volume"] > 0)
        )
        dataframe.loc[late_reclaim, ["enter_long", "enter_tag"]] = (1, "v122_late_reclaim")
        return dataframe


class _KhoaAnnualGrowthV142FrozenV145(Github_NAK09_Cryptocurrency_Trading_Project__KhoaAnnualGrowthV157__20261009_163020):
    """Frozen V122PF plus a causal weak-BTC/weak-volume stake throttle."""

    v142_fragile_enabled = BooleanParameter(default=True, space="buy", optimize=False)
    v142_btc_change3_ceiling = DecimalParameter(
        -0.050, 0.080, default=0.000, decimals=3, space="buy", optimize=False
    )
    v142_volume_ceiling = DecimalParameter(
        0.60, 1.20, default=0.90, decimals=2, space="buy", optimize=False
    )
    v142_fragile_stake_scale = DecimalParameter(
        0.10, 0.90, default=0.25, decimals=2, space="buy", optimize=False
    )

    def bot_start(self, **kwargs: Any) -> None:
        super().bot_start(**kwargs)
        for name in (
            "v142_fragile_enabled",
            "v142_btc_change3_ceiling",
            "v142_volume_ceiling",
            "v142_fragile_stake_scale",
        ):
            if name in self.config:
                getattr(self, name).value = self.config[name]

    def custom_stake_amount(
        self,
        pair: str,
        current_time: datetime,
        current_rate: float,
        proposed_stake: float,
        min_stake: float | None,
        max_stake: float,
        leverage: float,
        entry_tag: str | None,
        side: str,
        **kwargs: Any,
    ) -> float:
        stake = super().custom_stake_amount(
            pair,
            current_time,
            current_rate,
            proposed_stake,
            min_stake,
            max_stake,
            leverage,
            entry_tag,
            side,
            **kwargs,
        )
        if not bool(self.v142_fragile_enabled.value) or entry_tag != "macro_bull_hold":
            return stake

        candle = self._latest_candle(pair, current_time)
        btc_change3 = self._as_float(candle, "btc_change3_1d", 1.0)
        volume_ratio = self._as_float(candle, "volume_ratio", 10.0)
        fragile = (
            btc_change3 <= float(self.v142_btc_change3_ceiling.value)
            and volume_ratio <= float(self.v142_volume_ceiling.value)
        )
        if not fragile:
            return stake

        adjusted = min(
            float(stake) * float(self.v142_fragile_stake_scale.value),
            float(max_stake),
        )
        if min_stake is not None and adjusted > 0:
            adjusted = max(adjusted, float(min_stake))
        return adjusted


class _KhoaAnnualGrowthV144FrozenV145(_KhoaAnnualGrowthV142FrozenV145):
    """V142 with bounded exposure boosts for profitable recovery modules."""

    v144_scout_boost = DecimalParameter(
        1.00, 2.50, default=2.50, decimals=2, space="buy", optimize=False
    )
    v144_reclaim_boost = DecimalParameter(
        1.00, 1.10, default=1.087, decimals=3, space="buy", optimize=False
    )

    def bot_start(self, **kwargs: Any) -> None:
        super().bot_start(**kwargs)
        for name in ("v144_scout_boost", "v144_reclaim_boost"):
            if name in self.config:
                getattr(self, name).value = self.config[name]

    def custom_stake_amount(
        self,
        pair: str,
        current_time: datetime,
        current_rate: float,
        proposed_stake: float,
        min_stake: float | None,
        max_stake: float,
        leverage: float,
        entry_tag: str | None,
        side: str,
        **kwargs: Any,
    ) -> float:
        stake = super().custom_stake_amount(
            pair,
            current_time,
            current_rate,
            proposed_stake,
            min_stake,
            max_stake,
            leverage,
            entry_tag,
            side,
            **kwargs,
        )
        if entry_tag == "macro_bear_rebound_scout":
            stake *= float(self.v144_scout_boost.value)
        elif entry_tag == "post_capitulation_reclaim":
            stake *= float(self.v144_reclaim_boost.value)
        stake = min(float(stake), float(max_stake))
        if min_stake is not None and stake > 0:
            stake = max(stake, float(min_stake))
        return stake


class KhoaAnnualGrowthV157(_KhoaAnnualGrowthV144FrozenV145):
    use_custom_roi = True
    """V149 with an independently gated bull breakout research route."""

    v145_trend_scale = DecimalParameter(
        0.10, 1.00, default=0.50, decimals=2, space="buy", optimize=False
    )
    v145_late_scale = DecimalParameter(
        0.10, 1.00, default=0.25, decimals=2, space="buy", optimize=False
    )
    v146_macro_stake_scale = DecimalParameter(
        0.40, 1.00, default=1.00, decimals=2, space="buy", optimize=False
    )
    v146_macro_guard_enabled = BooleanParameter(default=False, space="sell", optimize=False)
    v146_macro_stop = DecimalParameter(
        0.06, 0.18, default=0.10, decimals=3, space="sell", optimize=False
    )
    v146_macro_fail_enabled = BooleanParameter(default=False, space="sell", optimize=False)
    v146_macro_fail_profit = DecimalParameter(
        -0.060, -0.015, default=-0.030, decimals=3, space="sell", optimize=False
    )
    v146_breakout_enabled = BooleanParameter(default=True, space="buy", optimize=False)
    v146_disable_late_reclaim = BooleanParameter(default=False, space="buy", optimize=False)
    v146_btc_bear_only = BooleanParameter(default=True, space="buy", optimize=False)
    v146_breakout_stake_scale = DecimalParameter(
        0.15, 0.75, default=0.35, decimals=2, space="buy", optimize=False
    )
    v146_breakout_stop = DecimalParameter(
        0.03, 0.10, default=0.06, decimals=3, space="sell", optimize=False
    )
    v146_breakout_volume_min = DecimalParameter(
        1.20, 8.00, default=5.00, decimals=2, space="buy", optimize=False
    )
    v146_breakout_extension_max = DecimalParameter(
        1.025, 1.090, default=1.055, decimals=3, space="buy", optimize=False
    )
    v146_base_range_max = DecimalParameter(
        0.025, 0.100, default=0.045, decimals=3, space="buy", optimize=False
    )
    v146_prior_return_max = DecimalParameter(
        0.010, 0.090, default=0.035, decimals=3, space="buy", optimize=False
    )
    v147_spike_volume_min = DecimalParameter(
        2.0, 5.0, default=3.0, decimals=2, space="buy", optimize=False
    )
    v147_spike_scale = DecimalParameter(
        0.25, 1.0, default=1.0, decimals=2, space="buy", optimize=False
    )
    v147_btc_drop_floor = DecimalParameter(
        -0.05, 0.02, default=0.0, decimals=3, space="buy", optimize=False
    )
    v147_btc_drop_scale = DecimalParameter(
        0.25, 1.0, default=1.0, decimals=2, space="buy", optimize=False
    )
    v148_require_valid_btc_ema200 = BooleanParameter(default=False, space="buy", optimize=False)
    v149_require_valid_slow_reference = BooleanParameter(default=True, space="buy", optimize=False)
    v149_require_ema100_warmup = BooleanParameter(default=False, space="buy", optimize=False)
    v150_bull_breakout_enabled = BooleanParameter(default=False, space="buy", optimize=False)
    v150_bull_stake_scale = DecimalParameter(
        0.10, 0.40, default=0.20, decimals=2, space="buy", optimize=False
    )
    v150_bull_btc_slow_min = DecimalParameter(
        1.00, 1.12, default=1.02, decimals=3, space="buy", optimize=False
    )
    v150_bull_btc_change3_floor = DecimalParameter(
        -0.08, 0.02, default=-0.03, decimals=3, space="buy", optimize=False
    )
    v151_bull_roi_hold_hours = IntParameter(
        0, 168, default=0, space="sell", optimize=False
    )

    def bot_start(self, **kwargs: Any) -> None:
        super().bot_start(**kwargs)
        self._v151_original_roi = dict(self.minimal_roi)
        if int(self.config.get("v151_bull_roi_hold_hours", 0)) > 0:
            self.minimal_roi = {0: 10.0}
        for name in (
            "v145_trend_scale", "v145_late_scale", "v146_macro_stake_scale",
            "v146_macro_stop", "v146_macro_guard_enabled", "v146_macro_fail_enabled",
            "v146_macro_fail_profit", "v146_breakout_enabled", "v146_disable_late_reclaim",
            "v146_btc_bear_only",
            "v146_breakout_stake_scale",
            "v146_breakout_stop", "v146_breakout_volume_min", "v146_breakout_extension_max",
            "v146_base_range_max", "v146_prior_return_max",
            "v147_spike_volume_min", "v147_spike_scale",
            "v147_btc_drop_floor", "v147_btc_drop_scale",
            "v148_require_valid_btc_ema200", "v149_require_valid_slow_reference",
            "v149_require_ema100_warmup",
            "v150_bull_breakout_enabled", "v150_bull_stake_scale",
            "v150_bull_btc_slow_min", "v150_bull_btc_change3_floor",
            "v151_bull_roi_hold_hours",
        ):
            if name in self.config:
                getattr(self, name).value = self.config[name]

    def custom_roi(
        self,
        pair: str,
        trade: Trade,
        current_time: datetime,
        trade_duration: int,
        entry_tag: str | None,
        side: str,
        **kwargs: Any,
    ) -> float | None:
        hold_hours = int(self._v146_value("v151_bull_roi_hold_hours"))
        if hold_hours <= 0:
            return None
        if entry_tag == "v150_bull_breakout":
            if trade_duration < 453:
                return 0.187
            if trade_duration < hold_hours * 60:
                return 0.075
            return 0.0
        for minute, threshold in sorted(
            ((int(minute), value) for minute, value in self._v151_original_roi.items()),
            reverse=True,
        ):
            if trade_duration >= minute:
                return float(threshold)
        return None

    def _v146_value(self, name: str) -> Any:
        return self.config.get(name, getattr(self, name).value)

    def custom_stake_amount(
        self,
        pair: str,
        current_time: datetime,
        current_rate: float,
        proposed_stake: float,
        min_stake: float | None,
        max_stake: float,
        leverage: float,
        entry_tag: str | None,
        side: str,
        **kwargs: Any,
    ) -> float:
        stake = super().custom_stake_amount(
            pair,
            current_time,
            current_rate,
            proposed_stake,
            min_stake,
            max_stake,
            leverage,
            entry_tag,
            side,
            **kwargs,
        )
        if entry_tag == "trend_strong_pullback":
            stake *= float(self._v146_value("v145_trend_scale"))
        elif entry_tag == "v122_late_reclaim":
            stake *= float(self._v146_value("v145_late_scale"))
        elif entry_tag == "macro_bull_hold":
            stake *= float(self._v146_value("v146_macro_stake_scale"))
            spike_scale = float(self._v146_value("v147_spike_scale"))
            btc_scale = float(self._v146_value("v147_btc_drop_scale"))
            if spike_scale < 1.0 or btc_scale < 1.0:
                try:
                    frame, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
                    candle = frame.iloc[-1]
                    if float(candle["volume_ratio"]) >= float(self._v146_value("v147_spike_volume_min")):
                        stake *= spike_scale
                    if float(candle["btc_change3_1d"]) < float(self._v146_value("v147_btc_drop_floor")):
                        stake *= btc_scale
                except (AttributeError, IndexError, KeyError, TypeError, ValueError):
                    return 0.0
        elif entry_tag == "v146_early_breakout":
            stake *= float(self._v146_value("v146_breakout_stake_scale"))
        elif entry_tag == "v150_bull_breakout":
            stake *= float(self._v146_value("v150_bull_stake_scale"))
        stake = min(float(stake), float(max_stake))
        if min_stake is not None and stake > 0:
            stake = max(stake, float(min_stake))
        return stake

    def custom_stoploss(
        self,
        pair: str,
        trade: Trade,
        current_time: datetime,
        current_rate: float,
        current_profit: float,
        after_fill: bool,
        **kwargs: Any,
    ) -> float | None:
        if trade.enter_tag in {"v146_early_breakout", "v150_bull_breakout"} or (
            trade.enter_tag == "macro_bull_hold" and bool(self._v146_value("v146_macro_guard_enabled"))
        ):
            risk = (
                float(self._v146_value("v146_macro_stop"))
                if trade.enter_tag == "macro_bull_hold"
                else float(self._v146_value("v146_breakout_stop"))
            )
            stop_price = trade.open_rate * (1.0 - risk)
            if current_rate <= stop_price:
                return 0.001
            return stoploss_from_absolute(stop_price, current_rate, is_short=False, leverage=1.0)
        return super().custom_stoploss(
            pair, trade, current_time, current_rate, current_profit, after_fill, **kwargs
        )

    def custom_exit(
        self,
        pair: str,
        trade: Trade,
        current_time: datetime,
        current_rate: float,
        current_profit: float,
        **kwargs: Any,
    ) -> str | bool | None:
        parent_exit = super().custom_exit(pair, trade, current_time, current_rate, current_profit, **kwargs)
        if parent_exit or trade.enter_tag != "macro_bull_hold":
            return parent_exit
        if not bool(self._v146_value("v146_macro_fail_enabled")):
            return None
        if current_profit > float(self._v146_value("v146_macro_fail_profit")):
            return None
        try:
            frame, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
            candle = frame.iloc[-1]
            prev = frame.iloc[-2]
            if (
                float(candle["close"]) < float(candle["ema20"])
                and float(candle["macdhist"]) < float(prev["macdhist"])
                and (float(candle["rsi"]) < 52 or float(candle["p4h_change3_4h"]) < -0.01)
            ):
                return "v146_macro_fail"
        except (AttributeError, IndexError, KeyError, TypeError, ValueError):
            return None
        return None

    def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        dataframe = super().populate_entry_trend(dataframe, metadata)
        if bool(self._v146_value("v146_disable_late_reclaim")):
            late_reclaim = dataframe["enter_tag"].eq("v122_late_reclaim")
            dataframe.loc[late_reclaim, ["enter_long", "enter_tag"]] = (0, None)
        previous_high = dataframe["high"].rolling(24).max().shift(1)
        previous_low = dataframe["low"].rolling(24).min().shift(1)
        previous_return = dataframe["close"].shift(1) / dataframe["close"].shift(25) - 1.0
        breakout_base = (
            dataframe["enter_long"].ne(1)
            & (dataframe["close"] > previous_high)
            & (previous_high / previous_low.replace(0, float("nan")) - 1.0
               < float(self._v146_value("v146_base_range_max")))
            & (previous_return < float(self._v146_value("v146_prior_return_max")))
            & (dataframe["close"] > dataframe["open"])
            & (dataframe["close"] > dataframe["ema20"])
            & (dataframe["ema8"] > dataframe["ema20"])
            & (dataframe["close"] / dataframe["ema20"].replace(0, float("nan"))
               < float(self._v146_value("v146_breakout_extension_max")))
            & (dataframe["rsi"] > 55)
            & (dataframe["rsi"] < 88)
            & (dataframe["macdhist"] > dataframe["macdhist"].shift(1))
            & (dataframe["volume_ratio"] > float(self._v146_value("v146_breakout_volume_min")))
            & (dataframe["close_location"] > 0.65)
            & (dataframe["close_4h"] > dataframe["p4h_ema20_4h"] * 0.985)
            & (dataframe["p4h_rsi_4h"] > 50)
            & (dataframe["btc_change3_1d"] > -0.08)
            & (dataframe["close_1d"] > dataframe["btc_slow_reference_1d"] * 0.85)
            & (dataframe["volume"] > 0)
        )
        bear_breakout = (
            bool(self._v146_value("v146_breakout_enabled"))
            & breakout_base
            & ((not bool(self._v146_value("v146_btc_bear_only")))
               | (dataframe["close_1d"] < dataframe["btc_slow_reference_1d"]))
        )
        dataframe.loc[bear_breakout, ["enter_long", "enter_tag"]] = (1, "v146_early_breakout")
        bull_breakout = (
            bool(self._v146_value("v150_bull_breakout_enabled"))
            & breakout_base
            & dataframe["enter_long"].ne(1)
            & (dataframe["close_1d"] > dataframe["btc_slow_reference_1d"]
               * float(self._v146_value("v150_bull_btc_slow_min")))
            & (dataframe["btc_change3_1d"]
               > float(self._v146_value("v150_bull_btc_change3_floor")))
            & (dataframe["close"].pct_change() < 0.03)
        )
        dataframe.loc[bull_breakout, ["enter_long", "enter_tag"]] = (1, "v150_bull_breakout")
        if bool(self._v146_value("v148_require_valid_btc_ema200")):
            btc_ema200_valid = dataframe["btc_ema200_1d"].gt(0) & np.isfinite(dataframe["btc_ema200_1d"])
            dataframe.loc[~btc_ema200_valid, ["enter_long", "enter_tag"]] = (0, None)
        if bool(self._v146_value("v149_require_valid_slow_reference")):
            reference_valid = dataframe["btc_slow_reference_1d"].gt(0) & np.isfinite(dataframe["btc_slow_reference_1d"])
            dataframe.loc[~reference_valid, ["enter_long", "enter_tag"]] = (0, None)
        if bool(self._v146_value("v149_require_ema100_warmup")):
            ema100_valid = dataframe["btc_ema100_1d"].gt(0) & np.isfinite(dataframe["btc_ema100_1d"])
            dataframe.loc[~ema100_valid, ["enter_long", "enter_tag"]] = (0, None)
        return dataframe

