# source: https://raw.githubusercontent.com/MiltiadesLefk/freqtrade-bot/aaa89ca0310c373c8aca0c36da472f741246f2fd/user_data/strategies/BTCTrendATR.py
# directory_url: https://github.com/MiltiadesLefk/freqtrade-bot/blob/main/user_data/strategies/
# User: MiltiadesLefk
# Repository: freqtrade-bot
# --------------------import logging
from datetime import datetime
from typing import Optional, Union
import talib.abstract as ta
from pandas import DataFrame
from typing import Optional
from freqtrade.strategy import IStrategy, IntParameter, DecimalParameter
from freqtrade.persistence import Trade

logger = logging.getLogger(__name__)


class Github_MiltiadesLefk_freqtrade_bot__BTCTrendATR__20261001_163252(IStrategy):
    """
    Cross-check of the grid's best surviving structure, in an INDEPENDENT engine.

    Structure: 1d, long/short trend (close vs SMA-n), take profit at 2xATR(14),
    no stop loss (the grid's marginal table said stops hurt; that table was
    later found to be contaminated, so this run is the honest re-test).

    My own pandas simulator produced +2019% for structures like this because it
    re-entered on the same bar a take-profit filled. After fixing that, the same
    walk-forward gave median PF 0.992 across 65 structures - i.e. break-even -
    with this cell in the surviving tail at PF 1.429 / 80 trades / +70.9%.
    Freqtrade is the arbiter: if it disagrees, the pandas number is still wrong.
    """

    INTERFACE_VERSION = 3
    timeframe = "1d"
    can_short = True
    stoploss = -0.99          # deliberately inert: the tested structure has no SL
    trailing_stop = False
    minimal_roi = {"0": 99}   # TP is ATR-based, handled in custom_exit
    process_only_new_candles = True
    use_exit_signal = True
    startup_candle_count = 450   # VOL_MED_WINDOW(365) + VOL_WINDOW(30) + slack

    order_types = {"entry": "market", "exit": "market",
                   "stoploss": "market", "stoploss_on_exchange": False}

    sma_period = IntParameter(20, 200, default=50, space="buy", optimize=True)
    tp_atr = DecimalParameter(1.0, 6.0, default=2.0, decimals=1, space="sell", optimize=True)
    # 0 = disabled. The full 20,941-config sweep ranked by time-block consistency
    # put tp=2-3 with sl=0-3 in the top cluster; sl often never binds at all.
    sl_atr = DecimalParameter(0.0, 6.0, default=0.0, decimals=1, space="sell", optimize=True)
    ATR_PERIOD = 14

    # Fixed-fractional sizing:each trade risks RISK_PCT of TOTAL equity if the
    # 2xATR stop is hit.  risk = notional x stop_distance = stake x lev x d,
    # so stake = equity x RISK_PCT / (lev x d).  Note notional is independent
    # of leverage here - leverage only changes the margin posted, not the risk.
    RISK_PCT = 0.01
    VOL_WINDOW = 30          # realised-vol lookback (bars)
    VOL_MED_WINDOW = 365     # trailing median of that vol = the target level
    FALLBACK_EQUITY_FRAC = 0.20

    def leverage(self, pair, current_time, current_rate, proposed_leverage,
                 max_leverage, entry_tag, side, **kwargs) -> float:
        return 1.0

    def custom_stake_amount(self, pair: str, current_time: datetime,
                            current_rate: float, proposed_stake: float,
                            min_stake: Optional[float], max_stake: float,
                            leverage: float, entry_tag: Optional[str],
                            side: str, **kwargs) -> float:
        equity = self.wallets.get_total(self.config["stake_currency"])
        if equity <= 0:
            return proposed_stake
        try:
            af = float(entry_tag)          # ATR as a fraction of price, set at entry
        except (TypeError, ValueError):
            af = None
        d = (self.sl_atr.value * af) if (af and self.sl_atr.value > 0) else None
        if not d or d <= 0:
            # No stop distance to size against -> stay small rather than guess.
            return min(proposed_stake, equity * self.FALLBACK_EQUITY_FRAC)
        stake = (equity * self.RISK_PCT) / (d * max(leverage, 1e-9))

        # Volatility targeting. Validated on data never used to pick the rules
        # (BTC spot 2017-2023 pre-sample + ETH): max drawdown 68.9% -> 61.3%
        # and return 211% -> 476%, i.e. MAR 3.02 -> 7.66 (2.54x).
        try:
            df, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
            if df is not None and len(df) and "vol_mult" in df:
                vm = float(df["vol_mult"].iloc[-1])
                if vm == vm and 0 < vm <= 1.0:
                    stake *= vm
                    logger.info(f"{pair}: vol-target multiplier {vm:.2f}")
        except Exception as ex:
            logger.warning(f"{pair}: vol_mult unavailable ({ex}) - using full risk size")

        stake = min(stake, equity * 0.95, max_stake)
        if min_stake and stake < min_stake:
            # Exchange minimum overrides the risk budget. Say so loudly: on a
            # small account this silently raises risk well above RISK_PCT.
            eff = (min_stake * leverage * d) / equity
            logger.warning(
                f"{pair}: risk-sized stake {stake:.2f} < exchange min {min_stake:.2f}. "
                f"Using min -> effective risk {eff*100:.2f}% of equity, not "
                f"{self.RISK_PCT*100:.2f}%. Fund at least "
                f"{min_stake * leverage * d / self.RISK_PCT:.0f} {self.config['stake_currency']} "
                f"to honour the risk budget."
            )
            stake = min_stake
        else:
            logger.info(f"{pair}: stake {stake:.2f} (risk {self.RISK_PCT*100:.1f}% "
                        f"of {equity:.2f}, stop {d*100:.2f}%)")
        return stake

    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        for p in self.sma_period.range:
            dataframe[f"sma_{p}"] = ta.SMA(dataframe, timeperiod=p)
        dataframe["atr"] = ta.ATR(dataframe, timeperiod=self.ATR_PERIOD)
        # ATR as a price fraction, carried in enter_tag so custom_exit can rebuild
        # the exact TP level that was armed at entry (same trick as V2's barriers).
        dataframe["atr_frac"] = dataframe["atr"] / dataframe["close"]
        return dataframe

    def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        sma = dataframe[f"sma_{self.sma_period.value}"]
        tag = dataframe["atr_frac"].round(5).astype(str)
        long_c = (dataframe["close"] > sma) & (dataframe["volume"] > 0) & dataframe["atr_frac"].notna()
        short_c = (dataframe["close"] < sma) & (dataframe["volume"] > 0) & dataframe["atr_frac"].notna()
        dataframe.loc[long_c, "enter_long"] = 1
        dataframe.loc[short_c, "enter_short"] = 1
        dataframe.loc[long_c | short_c, "enter_tag"] = tag
        return dataframe

    def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        sma = dataframe[f"sma_{self.sma_period.value}"]
        dataframe.loc[dataframe["close"] < sma, "exit_long"] = 1
        dataframe.loc[dataframe["close"] > sma, "exit_short"] = 1
        return dataframe

    def custom_exit(self, pair: str, trade: Trade, current_time: datetime,
                    current_rate: float, current_profit: float,
                    **kwargs) -> Optional[Union[str, bool]]:
        try:
            af = float(trade.enter_tag)
        except (TypeError, ValueError):
            return None
        if not (0 < af < 0.5):
            return None
        move = self.tp_atr.value * af
        slv = self.sl_atr.value
        if slv > 0:
            smove = slv * af
            if trade.is_short:
                if current_rate >= trade.open_rate * (1 + smove):
                    return "atr_sl"
            elif current_rate <= trade.open_rate * (1 - smove):
                return "atr_sl"
        # Compare on RATE, not fee-inclusive profit, so the TP fires exactly
        # where the tested structure said it does.
        if trade.is_short:
            if current_rate <= trade.open_rate * (1 - move):
                return "atr_tp"
        elif current_rate >= trade.open_rate * (1 + move):
            return "atr_tp"
        return None
