# source: https://raw.githubusercontent.com/thaodo2512/lstm_cnn/76d00409d44c9e63cef213386ffcfb5f2380da9c/freqtrade_user_data/strategies/ichi_v1.py
from __future__ import annotations

# Ichimoku + FreqAI hybrid strategy
# - Moves indicators into FreqAI feature hooks
# - Uses FreqAI predictions to gate entries
# - Trend filters via Ichimoku cloud and EMA fans

# --- Do not remove these libs ---
from freqtrade.strategy.interface import IStrategy
from pandas import DataFrame
import json
import talib.abstract as ta  # type: ignore
import freqtrade.vendor.qtpylib.indicators as qtpylib
import pandas as pd  # noqa

pd.options.mode.chained_assignment = None  # default='warn'
import technical.indicators as ftt  # type: ignore
from functools import reduce
import os


class Github_thaodo2512_lstm_cnn__ichi_v1__20251005_072617(IStrategy):
    """Ichimoku + FreqAI hybrid strategy.

    Notes
    - Subclasses IStrategy (FreqAI integrates via self.freqai.start()).
    - Predictions column name follows the label name set in set_freqai_targets (here: '&-s_close').
    - If FreqAI does not provide '&-s_close_mean'/'&-s_close_std', a rolling mean/std fallback is used.
    """

    # Buy hyperspace params (can be overridden via env, see _env_* helpers)
    buy_params = {
        "buy_trend_above_senkou_level": 1,
        # Original defaults were stricter; keep env overrides available
        "buy_trend_bullish_level": 6,
        "buy_fan_magnitude_shift_value": 3,
        "buy_min_fan_magnitude_gain": 1.002,
        # "buy_min_fan_magnitude_gain": 1.008,  # very safe (Win% ~90%), fewer trades
    }

    # Sell hyperspace params
    sell_params = {
        # Use EMA trend cross for exits
        "sell_trend_indicator": "trend_close_2h",
    }

    # ROI/Stoploss defaults closer to the original idea (can still override via config)
    minimal_roi = {
        "0": 0.059,
        "10": 0.037,
        "41": 0.012,
        "114": 0.0,
    }

    stoploss = -0.275

    # Optimal timeframe for the strategy
    timeframe = "5m"

    startup_candle_count = 96
    # FreqAI requires new-candle processing in live mode
    process_only_new_candles = True  # required for FreqAI in live

    trailing_stop = False
    # trailing_stop_positive = 0.002
    # trailing_stop_positive_offset = 0.025
    # trailing_only_offset_is_reached = True

    use_exit_signal = True
    # Use exit signal in combination with ROI table
    exit_profit_only = False
    ignore_roi_if_entry_signal = False
    # Enable shorting (requires futures-capable exchange/config)
    # Disabled by default; can re-enable via env gates below
    can_short: bool = False

    trend_period_map = {
        "trend_close_5m": 1,
        "trend_close_15m": 3,
        "trend_close_30m": 6,
        "trend_close_1h": 12,
        "trend_close_2h": 24,
        "trend_close_4h": 48,
        "trend_close_6h": 72,
        "trend_close_8h": 96,
    }

    freqai_info = {
        "model_per_pair": True,
        "feature_parameters": {
            "include_timeframes": ["5m", "15m", "30m", "1h", "2h", "4h"],
            "include_shifted_candles": 3,
            "include_corr_pairlist": [],
            "indicator_periods_candles": [1, 3, 6, 12, 24, 48, 72, 96],
            "label_period_candles": 24,
        },
        "data_split_parameters": {
            "test_size": 0.25,
        },
        "fit_live_predictions_candles": 300,
    }

    plot_config = {
        "main_plot": {
            # fill area between senkou_a and senkou_b
            "%%-senkou_a": {
                "color": "green",  # optional
                "fill_to": "%%-senkou_b",
                "fill_label": "Ichimoku Cloud",  # optional
                "fill_color": "rgba(255,76,46,0.2)",  # optional
            },
            # plot senkou_b, too. Not only the area to it.
            "%%-senkou_b": {},
            "%%-trend_close_period_1": {"color": "#FF5733"},
            "%%-trend_close_period_3": {"color": "#FF8333"},
            "%%-trend_close_period_6": {"color": "#FFB533"},
            "%%-trend_close_period_12": {"color": "#FFE633"},
            "%%-trend_close_period_24": {"color": "#E3FF33"},
            "%%-trend_close_period_48": {"color": "#C4FF33"},
            "%%-trend_close_period_72": {"color": "#61FF33"},
            "%%-trend_close_period_96": {"color": "#33FF7D"},
        },
        "subplots": {
            "fan_magnitude": {"%%-fan_magnitude": {}},
            "fan_magnitude_gain": {"%%-fan_magnitude_gain": {}},
        },
    }

    def __init__(self, config: dict) -> None:
        super().__init__(config)
        # Make short capability runtime-togglable via env.
        # If you set ICHI_ENABLE_SHORT=true in .env / compose env, shorts are allowed.
        self.can_short = self._env_bool("ICHI_ENABLE_SHORT", False)

    @property
    def protections(self):
        # Define protections in-strategy (config key deprecated in recent Freqtrade)
        return [
            {"method": "CooldownPeriod", "stop_duration_candles": 12},
            {
                "method": "MaxDrawdown",
                "lookback_period_candles": 144,
                "stop_duration_candles": 48,
                "max_allowed_drawdown": 0.06,
            },
        ]

    # -------------------- Env helpers --------------------
    @staticmethod
    def _env_float(key: str, default: float) -> float:
        try:
            v = os.getenv(key)
            return float(v) if v not in (None, "") else default
        except Exception:
            return default

    @staticmethod
    def _env_int(key: str, default: int) -> int:
        try:
            v = os.getenv(key)
            return int(v) if v not in (None, "") else default
        except Exception:
            return default

    @staticmethod
    def _env_bool(key: str, default: bool) -> bool:
        v = os.getenv(key)
        if v is None or v == "":
            return default
        return str(v).strip().lower() in {"1", "true", "yes", "y", "on"}

    @staticmethod
    def _env_str(key: str, default: str) -> str:
        v = os.getenv(key)
        return v if v not in (None, "") else default

    # -------------------- FreqAI feature hooks --------------------
    def feature_engineering_expand_all(
        self, dataframe: DataFrame, period: int, metadata: dict, **kwargs
    ) -> DataFrame:
        # Write explicit period-suffixed columns to avoid naming ambiguity across periods
        dataframe[f"%%-trend_close_period_{period}"] = ta.EMA(
            dataframe["close"], timeperiod=period
        )
        dataframe[f"%%-trend_open_period_{period}"] = ta.EMA(
            dataframe["open"], timeperiod=period
        )
        dataframe[f"%%-atr_period_{period}"] = ta.ATR(dataframe, timeperiod=period)
        return dataframe

    def feature_engineering_expand_basic(
        self, dataframe: DataFrame, metadata: dict, **kwargs
    ) -> DataFrame:
        dataframe["%-pct-change"] = dataframe["close"].pct_change()
        dataframe["%-raw_volume"] = dataframe["volume"]
        dataframe["%-raw_price"] = dataframe["close"]
        return dataframe

    def feature_engineering_standard(
        self, dataframe: DataFrame, metadata: dict, **kwargs
    ) -> DataFrame:
        """Add Ichimoku and ensure EMA fan features exist.

        Uses periods from freqai_info.feature_parameters.indicator_periods_candles
        if available; falls back to [1, 3, 6, 12, 24, 48, 72, 96].
        """
        if "date" in dataframe.columns:
            dataframe["%-day_of_week"] = (dataframe["date"].dt.dayofweek + 1) / 7
            dataframe["%-hour_of_day"] = (dataframe["date"].dt.hour + 1) / 25

        ichimoku = ftt.ichimoku(
            dataframe,
            conversion_line_period=20,
            base_line_periods=60,
            laggin_span=120,
            displacement=30,
        )
        dataframe["%%-chikou_span"] = ichimoku["chikou_span"]
        dataframe["%%-tenkan_sen"] = ichimoku["tenkan_sen"]
        dataframe["%%-kijun_sen"] = ichimoku["kijun_sen"]
        dataframe["%%-senkou_a"] = ichimoku["senkou_span_a"]
        dataframe["%%-senkou_b"] = ichimoku["senkou_span_b"]
        dataframe["%%-leading_senkou_span_a"] = ichimoku["leading_senkou_span_a"]
        dataframe["%%-leading_senkou_span_b"] = ichimoku["leading_senkou_span_b"]
        dataframe["%%-cloud_green"] = ichimoku["cloud_green"]
        dataframe["%%-cloud_red"] = ichimoku["cloud_red"]

        # Ensure required EMA fan columns exist even if expand_all hasn't populated them yet
        def _ema(series: pd.Series, n: int) -> pd.Series:
            try:
                return ta.EMA(series, timeperiod=n)
            except Exception:
                return series.ewm(span=max(1, n), adjust=False).mean()

        # Derive dynamic period list
        periods = (
            list(self.freqai_info.get("feature_parameters", {}).get("indicator_periods_candles", []))
            if hasattr(self, "freqai_info") else []
        )
        if not periods:
            periods = [1, 3, 6, 12, 24, 48, 72, 96]
        periods = sorted(set(int(p) for p in periods))

        for p in periods:
            # Prefer underscore naming; also support legacy hyphen names by aliasing
            ccol = f"%%-trend_close_period_{p}"
            ocol = f"%%-trend_open_period_{p}"
            if ccol not in dataframe.columns:
                legacy = f"%%-trend_close-period_{p}"
                if legacy in dataframe.columns:
                    dataframe[ccol] = dataframe[legacy]
                else:
                    dataframe[ccol] = _ema(dataframe["close"], p)
            if ocol not in dataframe.columns:
                legacy = f"%%-trend_open-period_{p}"
                if legacy in dataframe.columns:
                    dataframe[ocol] = dataframe[legacy]
                else:
                    dataframe[ocol] = _ema(dataframe["open"], p)

        # Fan magnitude and acceleration (safe if columns were just created)
        # Always compute canonical fast=12 and slow=96 to avoid degenerate fm==1.0
        for need in (12, 96):
            ccol = f"%%-trend_close_period_{need}"
            if ccol not in dataframe.columns:
                dataframe[ccol] = _ema(dataframe["close"], need)
        # Guard against zero/NaN denominator
        den = dataframe["%%-trend_close_period_96"].replace(0, 1e-8)
        dataframe["%%-fan_magnitude"] = dataframe["%%-trend_close_period_12"] / den
        dataframe["%%-fan_magnitude_gain"] = (
            dataframe["%%-fan_magnitude"] / dataframe["%%-fan_magnitude"].shift(1)
        )

        return dataframe

    def set_freqai_targets(
        self, dataframe: DataFrame, metadata: dict, **kwargs
    ) -> DataFrame:
        # Predict forward return over the label_period (mean-close ratio - 1)
        lp = int(self.freqai_info["feature_parameters"]["label_period_candles"])
        dataframe["&-s_close"] = (
            dataframe["close"].shift(-lp).rolling(lp).mean() / dataframe["close"] - 1
        )
        return dataframe

    # -------------------- Strategy hooks --------------------
    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        # Log full runtime config once per pair/timeframe for debugging
        self._log_config_once(metadata)
        if self._env_bool("ICHI_USE_HA", True):
            heikinashi = qtpylib.heikinashi(dataframe)
            dataframe["open"] = heikinashi["open"]
            # Optionally use HA close as well (disabled by default)
            if self._env_bool("ICHI_USE_HA_CLOSE", False):
                dataframe["close"] = heikinashi["close"]
            dataframe["high"] = heikinashi["high"]
            dataframe["low"] = heikinashi["low"]

        dataframe = self.freqai.start(dataframe, metadata, self)
        return dataframe

    def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        df = dataframe.copy()
        df["enter_long"] = 0
        df["enter_short"] = 0

        conditions = []

        # Integrate FreqAI prediction
        require_dp = self._env_bool("ICHI_REQUIRE_DOPREDICT", True)
        if require_dp and "do_predict" in df.columns:
            conditions.append(df["do_predict"] == 1)

        # Test mode: minimal gates to force trades for validation
        if self._env_bool("ICHI_TEST_LOOSE", False):
            thr = self._env_float("ICHI_TEST_THR", 0.0)
            if "&-s_close" in df.columns:
                long_cond = df["&-s_close"] > thr
                short_cond = df["&-s_close"] < -thr
            else:
                # Fallback if prediction column missing: simple momentum
                long_cond = df["close"] > df["close"].shift(1)
                short_cond = df["close"] < df["close"].shift(1)
            if require_dp and "do_predict" in df.columns:
                long_cond = long_cond & (df["do_predict"] == 1)
                short_cond = short_cond & (df["do_predict"] == 1)
            df.loc[long_cond.fillna(False), "enter_long"] = 1
            if self._env_bool("ICHI_ENABLE_SHORT", True):
                df.loc[short_cond.fillna(False), "enter_short"] = 1
            return df

        # Fallback for target statistics if not provided by FreqAI
        if "&-s_close_mean" not in df.columns or "&-s_close_std" not in df.columns:
            pred = df.get("&-s_close", pd.Series(float("nan"), index=df.index))
            roll = pred.rolling(100, min_periods=5)
            df["&-s_close_mean"] = roll.mean()
            df["&-s_close_std"] = roll.std(ddof=0)

        # Optional prediction thresholding (disabled by default to mirror original idea)
        if self._env_bool("ICHI_USE_PRED_THRESHOLD", False):
            std_mult = self._env_float("ICHI_STD_MULT", 0.5)
            df["target_roi"] = df["&-s_close_mean"] + df["&-s_close_std"] * std_mult
            if "&-s_close" in df.columns:
                conditions.append(df["&-s_close"] > df["target_roi"])

        # Trending market above cloud
        level = self._env_int(
            "ICHI_CLOUD_LEVEL", int(self.buy_params["buy_trend_above_senkou_level"])
        )
        periods = (
            list(self.freqai_info.get("feature_parameters", {}).get("indicator_periods_candles", []))
            if hasattr(self, "freqai_info") else []
        )
        if not periods:
            periods = [1, 3, 6, 12, 24, 48, 72, 96]
        periods = sorted(set(int(p) for p in periods))
        for p in periods[: max(0, min(level, len(periods)))]:
            conditions.append(df[f"%%-trend_close_period_{p}"] > df["%%-senkou_a"])
            conditions.append(df[f"%%-trend_close_period_{p}"] > df["%%-senkou_b"])

        # Trends bullish (close EMA above open EMA)
        bull_level = self._env_int(
            "ICHI_BULLISH_LEVEL", int(self.buy_params["buy_trend_bullish_level"])
        )
        for p in periods[: max(0, min(bull_level, len(periods)))]:
            conditions.append(df[f"%%-trend_close_period_{p}"] > df[f"%%-trend_open_period_{p}"])

        # Fan magnitude acceleration
        fan_gain = float(
            self._env_float(
                "ICHI_FAN_GAIN", float(self.buy_params["buy_min_fan_magnitude_gain"]) 
            )
        )
        # Clamp to avoid equality/zero-division edge cases
        fan_gain = max(1.0001, fan_gain)
        conditions.append(df["%%-fan_magnitude_gain"] >= fan_gain)
        conditions.append(df["%%-fan_magnitude"] > 1)
        fan_shift = int(
            self._env_int(
                "ICHI_FAN_SHIFT", int(self.buy_params["buy_fan_magnitude_shift_value"]) 
            )
        )
        fm = df["%%-fan_magnitude"].ffill().fillna(1.0)
        for x in range(fan_shift):
            conditions.append(fm.shift(x + 1) < fm)

        # Optional volume filter
        if self._env_bool("ICHI_VOLUME_FILTER", False):
            vol_win = max(1, self._env_int("ICHI_VOL_WINDOW", 20))
            vol_mult = max(0.0, float(self._env_float("ICHI_VOL_MULT", 1.0)))
            vol_ma = df["volume"].rolling(vol_win, min_periods=1).mean()
            conditions.append(df["volume"] > vol_ma * vol_mult)

        if conditions:
            df.loc[reduce(lambda x, y: x & y, conditions), "enter_long"] = 1

        # ---------------- Short entry (optional via ICHI_ENABLE_SHORT) ----------------
        if self._env_bool("ICHI_ENABLE_SHORT", True):
            short_conditions = []
            if require_dp and "do_predict" in df.columns:
                short_conditions.append(df["do_predict"] == 1)

            # Use same computed target_roi magnitude only if thresholding is enabled
            if (
                self._env_bool("ICHI_USE_PRED_THRESHOLD", False)
                and "&-s_close" in df.columns
                and "target_roi" in df.columns
            ):
                short_conditions.append(df["&-s_close"] < -df["target_roi"])

            # Below cloud by level
            for p in periods[: max(0, min(level, len(periods)))]:
                short_conditions.append(df[f"%%-trend_close_period_{p}"] < df["%%-senkou_a"])
                short_conditions.append(df[f"%%-trend_close_period_{p}"] < df["%%-senkou_b"])

            # Bearish EMAs (close EMA below open EMA)
            for p in periods[: max(0, min(bull_level, len(periods)))]:
                short_conditions.append(df[f"%%-trend_close_period_{p}"] < df[f"%%-trend_open_period_{p}"])

            # Fan decreasing and below 1
            short_conditions.append(df["%%-fan_magnitude"] < 1)
            # Mirror gain threshold: require contraction
            short_conditions.append(df["%%-fan_magnitude_gain"] <= (1.0 / fan_gain))
            for x in range(fan_shift):
                short_conditions.append(fm.shift(x + 1) > fm)

            if short_conditions:
                df.loc[reduce(lambda x, y: x & y, short_conditions), "enter_short"] = 1

        return df

    def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        df = dataframe.copy()
        df["exit_long"] = 0
        df["exit_short"] = 0

        sell_indicator = self._env_str(
            "ICHI_SELL_TREND", self.sell_params["sell_trend_indicator"]
        )
        period = int(
            self.trend_period_map.get(
                sell_indicator, self.trend_period_map[self.sell_params["sell_trend_indicator"]]
            )
        )

        # EMA trend cross (legacy)
        # Map requested periods to nearest available to avoid KeyErrors when
        # indicator_periods_candles does not include exact values like 1 or 24
        periods = (
            list(self.freqai_info.get("feature_parameters", {}).get("indicator_periods_candles", []))
            if hasattr(self, "freqai_info") else []
        )
        if not periods:
            periods = [1, 3, 6, 12, 24, 48, 72, 96]
        periods = sorted(set(int(p) for p in periods))
        def _nearest(target: int, avail: list[int]) -> int:
            if target in avail:
                return target
            return min(avail, key=lambda x: abs(x - target)) if avail else target
        p_fast = _nearest(1, periods)
        p_slow = _nearest(period, periods)
        ema_cross_down = qtpylib.crossed_below(
            df[f"%%-trend_close_period_{p_fast}"], df[f"%%-trend_close_period_{p_slow}"]
        )

        # Ichimoku exits: Tenkan crosses below Kijun OR close below Kijun
        tenkan = df.get("%%-tenkan_sen")
        kijun = df.get("%%-kijun_sen")
        ichi_cross_down = (
            qtpylib.crossed_below(tenkan, kijun) if tenkan is not None and kijun is not None else pd.Series(False, index=df.index)
        )
        close_below_kijun = (
            (df["close"] < kijun) if kijun is not None else pd.Series(False, index=df.index)
        )

        # Exit mode: ema | ichi | kijun | any | all
        exit_mode = self._env_str("ICHI_EXIT_MODE", "any").strip().lower()
        if exit_mode == "ema":
            cond_long = ema_cross_down
        elif exit_mode in ("ichi", "tenkan_kijun_cross"):
            cond_long = (ichi_cross_down | close_below_kijun)
        elif exit_mode == "kijun":
            cond_long = close_below_kijun
        elif exit_mode == "all":
            cond_long = (ema_cross_down & ichi_cross_down & close_below_kijun)
        else:  # any
            cond_long = (ema_cross_down | ichi_cross_down | close_below_kijun)
        df.loc[cond_long.fillna(False), "exit_long"] = 1

        # Short exit (optional via ICHI_ENABLE_SHORT): crossed above inverses
        if self._env_bool("ICHI_ENABLE_SHORT", True):
            ema_cross_up = qtpylib.crossed_above(
                df[f"%%-trend_close_period_{p_fast}"], df[f"%%-trend_close_period_{p_slow}"]
            )
            ichi_cross_up = (
                qtpylib.crossed_above(tenkan, kijun) if tenkan is not None and kijun is not None else pd.Series(False, index=df.index)
            )
            close_above_kijun = (
                (df["close"] > kijun) if kijun is not None else pd.Series(False, index=df.index)
            )
            cond_short = (ema_cross_up | ichi_cross_up | close_above_kijun)
            df.loc[cond_short.fillna(False), "exit_short"] = 1
        # Optional debug export for inspection
        self._maybe_debug_export(df, metadata)
        return df

    # ---------------- Debug helpers ----------------
    @staticmethod
    def _safe_name(text: str) -> str:
        try:
            return "".join(c if c.isalnum() or c in ("-", "_") else "_" for c in str(text))
        except Exception:
            return "unknown"

    _debug_written_keys: set = set()

    def _maybe_debug_export(self, dataframe: DataFrame, metadata: dict) -> None:
        """If ICHI_DEBUG_EXPORT=true, write a compact CSV with predictions, gates, and
        final signals to /freqtrade/user_data/backtest_results.
        """
        flag = os.getenv("ICHI_DEBUG_EXPORT", "").strip().lower()
        if flag not in {"1", "true", "yes", "on"}:
            return
        try:
            pair = metadata.get("pair", "unknown_pair")
            timeframe = getattr(self, "timeframe", "tf")
            key = f"{pair}|{timeframe}"
            # Write once per pair/timeframe per run
            if key in self._debug_written_keys:
                return

            cols = []
            # price/volume
            cols += [c for c in ["open", "high", "low", "close", "volume"] if c in dataframe.columns]
            # predictions
            cols += [c for c in ["&-s_close", "&-s_close_mean", "&-s_close_std", "target_roi", "do_predict"] if c in dataframe.columns]
            # ichimoku
            cols += [c for c in [
                "%-pct-change",
                "%%-tenkan_sen", "%%-kijun_sen", "%%-senkou_a", "%%-senkou_b",
                "%%-cloud_green", "%%-cloud_red",
            ] if c in dataframe.columns]
            # trend / fan (use dynamic periods if available)
            dyn_periods = (
                list(self.freqai_info.get("feature_parameters", {}).get("indicator_periods_candles", []))
                if hasattr(self, "freqai_info") else []
            )
            if not dyn_periods:
                dyn_periods = [1,3,6,12,24,48,72,96]
            for p in sorted(set(int(p) for p in dyn_periods)):
                tc = f"%%-trend_close_period_{p}"
                to = f"%%-trend_open_period_{p}"
                if tc in dataframe.columns: cols.append(tc)
                if to in dataframe.columns: cols.append(to)
            cols += [c for c in ["%%-fan_magnitude", "%%-fan_magnitude_gain"] if c in dataframe.columns]
            # signals
            cols += [c for c in ["enter_long", "enter_short", "exit_long", "exit_short"] if c in dataframe.columns]

            debug_df = dataframe[cols].copy() if cols else dataframe.copy()

            # Limit rows if requested
            try:
                max_rows = int(os.getenv("ICHI_DEBUG_MAX_ROWS", "0"))
            except Exception:
                max_rows = 0
            if max_rows and len(debug_df) > max_rows:
                debug_df = debug_df.tail(max_rows)

            from pathlib import Path
            out_dir = Path("/freqtrade/user_data/backtest_results")
            out_dir.mkdir(parents=True, exist_ok=True)
            pair_s = self._safe_name(pair)
            tf_s = self._safe_name(timeframe)
            start = debug_df.index.min()
            end = debug_df.index.max()
            start_s = self._safe_name(str(start)[:19].replace(" ", "T")) if start is not None else "start"
            end_s = self._safe_name(str(end)[:19].replace(" ", "T")) if end is not None else "end"
            out_path = out_dir / f"ichi_debug_{pair_s}_{tf_s}_{start_s}_{end_s}.csv"
            debug_df.to_csv(out_path)
            self._debug_written_keys.add(key)
        except Exception:
            # Never interrupt strategy execution due to debugging
            return

    # ---------------- Config logging ----------------
    _config_logged_keys: set = set()

    def _log_config_once(self, metadata: dict) -> None:
        """Print a compact view of Github_thaodo2512_lstm_cnn__ichi_v1__20251005_072617 configuration and env overrides once.

        Toggle via env ICHI_LOG_CONFIG (default: true). Avoids log spam by logging
        once per pair/timeframe.
        """
        if not self._env_bool("ICHI_LOG_CONFIG", True):
            return
        try:
            pair = metadata.get("pair", "unknown_pair")
            timeframe = getattr(self, "timeframe", "tf")
            key = f"{pair}|{timeframe}"
            if key in self._config_logged_keys:
                return

            # Pull dynamic periods for fan/trend
            periods = (
                list(self.freqai_info.get("feature_parameters", {}).get("indicator_periods_candles", []))
                if hasattr(self, "freqai_info") else []
            )
            if not periods:
                periods = [1, 3, 6, 12, 24, 48, 72, 96]

            env_overrides = {
                "ICHI_STD_MULT": self._env_float("ICHI_STD_MULT", 0.5),
                "ICHI_CLOUD_LEVEL": self._env_int("ICHI_CLOUD_LEVEL", int(self.buy_params["buy_trend_above_senkou_level"])),
                "ICHI_BULLISH_LEVEL": self._env_int("ICHI_BULLISH_LEVEL", int(self.buy_params["buy_trend_bullish_level"])),
                "ICHI_FAN_SHIFT": self._env_int("ICHI_FAN_SHIFT", int(self.buy_params["buy_fan_magnitude_shift_value"])),
                "ICHI_FAN_GAIN": self._env_float("ICHI_FAN_GAIN", float(self.buy_params["buy_min_fan_magnitude_gain"])),
                "ICHI_REQUIRE_DOPREDICT": self._env_bool("ICHI_REQUIRE_DOPREDICT", True),
                "ICHI_USE_HA": self._env_bool("ICHI_USE_HA", True),
                "ICHI_USE_HA_CLOSE": self._env_bool("ICHI_USE_HA_CLOSE", False),
                "ICHI_ENABLE_SHORT": self._env_bool("ICHI_ENABLE_SHORT", False),
                "ICHI_TEST_LOOSE": self._env_bool("ICHI_TEST_LOOSE", False),
                "ICHI_TEST_THR": self._env_float("ICHI_TEST_THR", 0.0),
                "ICHI_SELL_TREND": self._env_str("ICHI_SELL_TREND", self.sell_params["sell_trend_indicator"]),
                "ICHI_EXIT_MODE": self._env_str("ICHI_EXIT_MODE", "any"),
                "ICHI_VOLUME_FILTER": self._env_bool("ICHI_VOLUME_FILTER", False),
                "ICHI_VOL_WINDOW": self._env_int("ICHI_VOL_WINDOW", 20),
                "ICHI_VOL_MULT": self._env_float("ICHI_VOL_MULT", 1.0),
            }

            freqai_cfg = getattr(self, "freqai_info", {})
            cfg = {
                "pair": pair,
                "timeframe": timeframe,
                "startup_candle_count": self.startup_candle_count,
                "stoploss": self.stoploss,
                "trailing": {
                    "enabled": bool(self.trailing_stop),
                    "positive": getattr(self, "trailing_stop_positive", None),
                    "offset": getattr(self, "trailing_stop_positive_offset", None),
                    "only_offset_reached": getattr(self, "trailing_only_offset_is_reached", None),
                },
                "minimal_roi": self.minimal_roi,
                "can_short": bool(self.can_short),
                "buy_params": self.buy_params,
                "sell_params": self.sell_params,
                "indicator_periods": periods,
                "env_overrides": env_overrides,
                "freqai_info": {
                    k: freqai_cfg.get(k)
                    for k in ("feature_parameters", "data_split_parameters", "fit_live_predictions_candles")
                    if isinstance(freqai_cfg, dict)
                },
            }
            payload = json.dumps(cfg, default=str, indent=2)
            self.logger.info("Github_thaodo2512_lstm_cnn__ichi_v1__20251005_072617 config (%%s %%s) =>\n%%s", pair, timeframe, payload)
            self._config_logged_keys.add(key)
        except Exception:
            # Never block execution due to logging issues
            return
