# source: https://raw.githubusercontent.com/siriushex/trade.2/aaf51341a176bc9587c59c84be0eebf360f0c5d6/user_data/strategies/BtcShortHorizonFreqaiStrategy.py
import logging
import sqlite3
from datetime import datetime, timedelta
from functools import lru_cache
from pathlib import Path

import numpy as np
import pandas as pd
import talib.abstract as ta
from pandas import DataFrame
from technical import qtpylib

from freqtrade.persistence import Trade
from freqtrade.strategy import IStrategy


logger = logging.getLogger(__name__)
USER_DATA_DIR = Path("/Users/mac/trade/user_data")
FUTURES_DATA_DIR = USER_DATA_DIR / "data" / "bitget" / "futures"
POLYMARKET_RUNTIME_DB_PATH = Path("/Users/mac/codex_runtime/btc_short_horizon_ml/runtime/decision_collector.sqlite3")
DEFAULT_PRICE_TO_BEAT_PROXY_OFFSET_USD = 40.83


def _safe_div(numerator: pd.Series, denominator: pd.Series) -> pd.Series:
    return numerator / denominator.replace(0.0, np.nan)


def _pick_first_populated_column(dataframe: DataFrame, candidates: list[str]) -> str | None:
    scored: list[tuple[float, str]] = []
    for candidate in candidates:
        if candidate not in dataframe.columns:
            continue
        series = pd.to_numeric(dataframe[candidate], errors="coerce").dropna()
        if series.empty:
            continue
        scored.append((float(series.abs().sum()), candidate))
    if not scored:
        return None
    scored.sort(reverse=True)
    return scored[0][1]


def _read_local_feather(path: Path, columns: list[str] | None = None) -> DataFrame:
    if not path.exists():
        return DataFrame()
    try:
        if columns:
            return pd.read_feather(path, columns=columns)
        return pd.read_feather(path)
    except Exception:
        return DataFrame()


@lru_cache(maxsize=4)
def _cached_polymarket_price_to_beat_lookup(db_mtime: float) -> dict[int, float]:
    if not POLYMARKET_RUNTIME_DB_PATH.exists():
        return {}
    try:
        conn = sqlite3.connect(str(POLYMARKET_RUNTIME_DB_PATH))
        frame = pd.read_sql_query(
            """
            SELECT event_slug, price_to_beat, recorded_at_utc
            FROM contour_decisions
            WHERE contour_id = 'polymarket_v2'
              AND decision_phase = 'round_signal'
              AND price_to_beat IS NOT NULL
            ORDER BY recorded_at_utc ASC
            """,
            conn,
        )
    except Exception:
        return {}
    finally:
        try:
            conn.close()
        except Exception:
            pass
    if frame.empty:
        return {}
    suffix = frame["event_slug"].astype(str).str.extract(r"(\d+)$")[0]
    frame["bucket_start_epoch"] = pd.to_numeric(suffix, errors="coerce").astype("Int64")
    frame["price_to_beat"] = pd.to_numeric(frame["price_to_beat"], errors="coerce")
    frame = frame.dropna(subset=["bucket_start_epoch", "price_to_beat"])
    if frame.empty:
        return {}
    frame = frame.drop_duplicates(subset=["bucket_start_epoch"], keep="last")
    return {
        int(row["bucket_start_epoch"]): float(row["price_to_beat"])
        for _, row in frame.iterrows()
    }


def _polymarket_price_to_beat_lookup() -> dict[int, float]:
    if not POLYMARKET_RUNTIME_DB_PATH.exists():
        return {}
    try:
        return _cached_polymarket_price_to_beat_lookup(POLYMARKET_RUNTIME_DB_PATH.stat().st_mtime)
    except OSError:
        return {}


class Github_siriushex_trade_2__BtcShortHorizonFreqaiStrategy__20260315_105111(IStrategy):
    """
    BTC-only short-horizon FreqAI strategy aligned with the bundled Polymarket project.
    Focus:
    - one pair: BTC/USDT:USDT
    - base timeframe: 1m
    - informative timeframes: 5m / 15m / 1h
    - targets: 2m / 5m / 10m / 15m directional classification with explicit NO_EDGE class
    """

    timeframe = "1m"
    can_short = True
    process_only_new_candles = True
    use_exit_signal = True
    startup_candle_count: int = 80

    minimal_roi = {
        "0": 0.007,
        "8": 0.003,
        "15": 0.0,
    }
    stoploss = -0.0075

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

    def informative_pairs(self):
        if not self.dp:
            return []
        pairs = self.dp.current_whitelist() or self.config.get("exchange", {}).get("pair_whitelist", [])
        informative: list[tuple[str, str, str]] = []
        for pair in pairs:
            informative.append((pair, "5m", "mark"))
            informative.append((pair, "1h", "mark"))
            informative.append((pair, "1h", "funding_rate"))
        return informative

    def feature_engineering_expand_all(
        self, dataframe: DataFrame, period: int, metadata: dict, **kwargs
    ) -> DataFrame:
        close = dataframe["close"]
        high = dataframe["high"]
        low = dataframe["low"]
        volume = dataframe["volume"]

        one_bar_return = close.pct_change()
        ema = ta.EMA(dataframe, timeperiod=period)
        atr = ta.ATR(dataframe, timeperiod=period)
        lowest_low = low.rolling(period).min()
        highest_high = high.rolling(period).max()
        range_span = (highest_high - lowest_low).replace(0.0, np.nan)

        dataframe["%-ret-period"] = close.pct_change(period)
        dataframe["%-logret-period"] = np.log(_safe_div(close, close.shift(period)))
        dataframe["%-volatility-period"] = one_bar_return.rolling(period).std(ddof=0)
        dataframe["%-ema_gap-period"] = _safe_div(close, ema) - 1.0
        dataframe["%-rsi-period"] = ta.RSI(dataframe, timeperiod=period) / 100.0
        dataframe["%-range_pct-period"] = _safe_div(highest_high, lowest_low) - 1.0
        dataframe["%-close_pos-period"] = _safe_div(close - lowest_low, range_span)
        dataframe["%-volume_ratio-period"] = _safe_div(volume, volume.rolling(period).mean())
        dataframe["%-atr_ratio-period"] = _safe_div(atr, close)
        return dataframe

    def feature_engineering_expand_basic(
        self, dataframe: DataFrame, metadata: dict, **kwargs
    ) -> DataFrame:
        close = dataframe["close"]
        open_ = dataframe["open"]
        high = dataframe["high"]
        low = dataframe["low"]
        volume = dataframe["volume"]

        logret = np.log(_safe_div(close, close.shift(1)))
        rolling_vwap = _safe_div((qtpylib.typical_price(dataframe) * volume).rolling(20).sum(), volume.rolling(20).sum())
        obv = ta.OBV(dataframe)
        volume_ma_20 = volume.rolling(20).mean()
        impulse_up_3 = close.pct_change(3)
        recent_high_10 = high.rolling(10).max().shift(1)
        recent_high_20 = high.rolling(20).max().shift(1)
        recent_low_10 = low.rolling(10).min().shift(1)
        recent_low_20 = low.rolling(20).min().shift(1)

        dataframe["%-ret_1"] = close.pct_change(1)
        dataframe["%-logret_1"] = logret
        dataframe["%-body_pct"] = _safe_div(close - open_, close)
        dataframe["%-hl_spread"] = _safe_div(high - low, close)
        dataframe["%-upper_wick_pct"] = _safe_div(high - np.maximum(open_, close), close)
        dataframe["%-lower_wick_pct"] = _safe_div(np.minimum(open_, close) - low, close)
        dataframe["%-realized_vol_1"] = logret.abs()
        dataframe["%-price_accel_1"] = close.pct_change(1).diff(1)
        dataframe["%-obv_delta_1"] = obv.diff(1)
        dataframe["%-vwap_distance_20"] = _safe_div(close, rolling_vwap) - 1.0
        dataframe["%-volume_thrust_20"] = _safe_div(volume, volume_ma_20)
        dataframe["%-impulse_up_3m"] = impulse_up_3
        dataframe["%-breakout_up_10"] = (_safe_div(close, recent_high_10) - 1.0).fillna(0.0)
        dataframe["%-breakout_up_20"] = (_safe_div(close, recent_high_20) - 1.0).fillna(0.0)
        dataframe["%-breakout_down_10"] = (_safe_div(close, recent_low_10) - 1.0).fillna(0.0)
        dataframe["%-breakout_down_20"] = (_safe_div(close, recent_low_20) - 1.0).fillna(0.0)
        dataframe["%-raw_volume"] = volume
        dataframe["%-raw_price"] = close
        return dataframe

    def feature_engineering_standard(
        self, dataframe: DataFrame, metadata: dict, **kwargs
    ) -> DataFrame:
        dataframe = dataframe.copy()
        close = dataframe["close"]
        open_ = dataframe["open"]
        high = dataframe["high"]
        low = dataframe["low"]
        volume = dataframe["volume"]

        ema_5 = ta.EMA(dataframe, timeperiod=5)
        ema_10 = ta.EMA(dataframe, timeperiod=10)
        ema_20 = ta.EMA(dataframe, timeperiod=20)
        ema_50 = ta.EMA(dataframe, timeperiod=50)

        bb_middle = close.rolling(20).mean()
        bb_std = close.rolling(20).std(ddof=0)
        rolling_vwap = _safe_div((qtpylib.typical_price(dataframe) * volume).rolling(20).sum(), volume.rolling(20).sum())
        obv = ta.OBV(dataframe)
        macd = ta.MACD(dataframe)
        donchian_high = high.rolling(3).max().shift(1)
        donchian_low = low.rolling(3).min().shift(1)
        realized_vol_1 = np.log(_safe_div(close, close.shift(1))).abs()
        volume_thrust = _safe_div(volume, volume.rolling(20).mean())
        recent_high_10 = high.rolling(10).max().shift(1)
        recent_high_20 = high.rolling(20).max().shift(1)
        recent_low_10 = low.rolling(10).min().shift(1)
        recent_low_20 = low.rolling(20).min().shift(1)
        breakout_up_10 = (_safe_div(close, recent_high_10) - 1.0).fillna(0.0)
        breakout_up_20 = (_safe_div(close, recent_high_20) - 1.0).fillna(0.0)
        breakout_down_10 = (_safe_div(close, recent_low_10) - 1.0).fillna(0.0)
        breakout_down_20 = (_safe_div(close, recent_low_20) - 1.0).fillna(0.0)
        bullish_body_3 = _safe_div((close - open_).rolling(3).sum(), close.rolling(3).mean())
        bearish_body_3 = _safe_div((open_ - close).rolling(3).sum(), close.rolling(3).mean())

        dataframe["%-ema_gap_5_20"] = _safe_div(ema_5, ema_20) - 1.0
        dataframe["%-ema_gap_10_50"] = _safe_div(ema_10, ema_50) - 1.0
        dataframe["%-bollinger_bandwidth_20"] = _safe_div(4.0 * bb_std, bb_middle)
        dataframe["%-rsi_1m_standard"] = ta.RSI(dataframe, timeperiod=14) / 100.0
        dataframe["%-macd_hist_slope_1m"] = macd["macdhist"].diff(1)
        dataframe["%-obv_delta_1m"] = obv.diff(1)
        dataframe["%-vwap_distance_1m"] = _safe_div(close, rolling_vwap) - 1.0
        # FreqAI can call this hook before expand_basic has materialized %-realized_vol_1.
        dataframe["%-realized_vol_3m"] = realized_vol_1.rolling(3).mean()
        dataframe["%-vol_of_vol_3m"] = realized_vol_1.rolling(3).std(ddof=0)
        dataframe["%-donchian_breakout_3m"] = np.select(
            [close > donchian_high, close < donchian_low],
            [1.0, -1.0],
            default=0.0,
        )
        dataframe["%-trend_pressure_1m"] = (
            (dataframe["%-ema_gap_5_20"] * 0.6) + (dataframe["%-ema_gap_10_50"] * 0.4)
        )
        dataframe["%-micro_momentum_balance_1m"] = (
            (dataframe["%-rsi_1m_standard"] - 0.5) * 1.5
            + np.tanh(dataframe["%-macd_hist_slope_1m"] / 8.0) * 0.8
            + np.tanh(dataframe["%-vwap_distance_1m"] * 800.0) * 0.6
            + (dataframe["%-donchian_breakout_3m"] * 0.35)
        )
        dataframe["%-squeeze_breakout_bias_1m"] = (
            dataframe["%-donchian_breakout_3m"] * dataframe["%-bollinger_bandwidth_20"]
        )
        dataframe["%-volatility_state_1m"] = (
            _safe_div(
                dataframe["%-realized_vol_3m"],
                dataframe["%-realized_vol_3m"].rolling(20).mean(),
            ).replace([np.inf, -np.inf], np.nan).fillna(0.0)
        )
        dataframe["%-breakout_up_10_1m"] = breakout_up_10
        dataframe["%-breakout_up_20_1m"] = breakout_up_20
        dataframe["%-breakout_down_10_1m"] = breakout_down_10
        dataframe["%-breakout_down_20_1m"] = breakout_down_20
        dataframe["%-volume_thrust_1m"] = volume_thrust
        dataframe["%-bullish_body_pressure_3m"] = bullish_body_3.fillna(0.0)
        dataframe["%-bearish_body_pressure_3m"] = bearish_body_3.fillna(0.0)
        dataframe["%-up_breakout_impulse_1m"] = (
            np.tanh(breakout_up_10 * 900.0) * 0.45
            + np.tanh(breakout_up_20 * 700.0) * 0.25
            + np.tanh(volume_thrust.sub(1.0).fillna(0.0) * 1.6) * 0.15
            + np.tanh(bullish_body_3.fillna(0.0) * 55.0) * 0.15
        )
        dataframe["%-down_breakout_impulse_1m"] = (
            np.tanh((-breakout_down_10) * 900.0) * 0.45
            + np.tanh((-breakout_down_20) * 700.0) * 0.25
            + np.tanh(volume_thrust.sub(1.0).fillna(0.0) * 1.6) * 0.15
            + np.tanh(bearish_body_3.fillna(0.0) * 55.0) * 0.15
        )

        date_index = pd.to_datetime(dataframe["date"], utc=True, errors="coerce")
        dataframe["%-minute_sin"] = np.sin(2.0 * np.pi * date_index.dt.minute / 60.0)
        dataframe["%-minute_cos"] = np.cos(2.0 * np.pi * date_index.dt.minute / 60.0)
        dataframe["%-hour_sin"] = np.sin(2.0 * np.pi * date_index.dt.hour / 24.0)
        dataframe["%-hour_cos"] = np.cos(2.0 * np.pi * date_index.dt.hour / 24.0)
        dataframe["%-weekday_sin"] = np.sin(2.0 * np.pi * date_index.dt.dayofweek / 7.0)
        dataframe["%-weekday_cos"] = np.cos(2.0 * np.pi * date_index.dt.dayofweek / 7.0)

        dataframe["%%rsi_1m"] = dataframe["%-rsi_1m_standard"]
        dataframe["%%macd_hist_slope_1m"] = dataframe["%-macd_hist_slope_1m"]
        dataframe["%%vwap_distance_1m"] = dataframe["%-vwap_distance_1m"]
        dataframe["%%donchian_breakout_3m"] = dataframe["%-donchian_breakout_3m"]
        dataframe["%%ema_gap_5_20"] = dataframe["%-ema_gap_5_20"]
        dataframe["%%ema_gap_10_50"] = dataframe["%-ema_gap_10_50"]
        dataframe["%%bb_width_20"] = dataframe["%-bollinger_bandwidth_20"]
        dataframe["%%trend_pressure_1m"] = dataframe["%-trend_pressure_1m"]
        dataframe["%%micro_momentum_balance_1m"] = dataframe["%-micro_momentum_balance_1m"]
        dataframe["%%volatility_state_1m"] = dataframe["%-volatility_state_1m"]
        dataframe["%%up_breakout_impulse_1m"] = dataframe["%-up_breakout_impulse_1m"]
        dataframe["%%down_breakout_impulse_1m"] = dataframe["%-down_breakout_impulse_1m"]
        dataframe["%%breakout_up_20_1m"] = dataframe["%-breakout_up_20_1m"]
        dataframe["%%breakout_down_20_1m"] = dataframe["%-breakout_down_20_1m"]
        dataframe["%%volume_thrust_1m"] = dataframe["%-volume_thrust_1m"]

        dataframe = self._merge_mark_and_funding_features(dataframe, metadata)
        return dataframe

    def _merge_mark_and_funding_features(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        pair = metadata["pair"]
        merged = dataframe.sort_values("date").copy()

        def load_market_frame(timeframe: str, candle_type: str) -> DataFrame:
            if self.dp:
                try:
                    frame = self.dp.get_pair_dataframe(pair=pair, timeframe=timeframe, candle_type=candle_type)
                except Exception:
                    frame = DataFrame()
                if frame is not None and not frame.empty:
                    return frame
            local_path = FUTURES_DATA_DIR / f"BTC_USDT_USDT-{timeframe}-{candle_type}.feather"
            return _read_local_feather(local_path)

        mark_5m_frame = load_market_frame("5m", "mark")
        if mark_5m_frame is None or mark_5m_frame.empty:
            merged["mark_open_5m"] = np.nan
            merged["mark_close_5m"] = np.nan
            merged["mark_source_date_5m"] = pd.NaT
        else:
            mark_5m_frame = mark_5m_frame.loc[:, ["date", "open", "close"]].copy()
            mark_5m_frame["date"] = pd.to_datetime(mark_5m_frame["date"], utc=True, errors="coerce")
            mark_5m_frame = mark_5m_frame.dropna(subset=["date"]).sort_values("date")
            mark_5m_frame = mark_5m_frame.rename(
                columns={
                    "open": "mark_open_5m",
                    "close": "mark_close_5m",
                    "date": "mark_source_date_5m",
                }
            )
            mark_5m_frame["date"] = mark_5m_frame["mark_source_date_5m"]
            merged = pd.merge_asof(merged, mark_5m_frame, on="date", direction="backward")

        mark_frame = load_market_frame("1h", "mark")
        if mark_frame is None or mark_frame.empty:
            merged["mark_close_1h"] = np.nan
            merged["mark_source_date_1h"] = pd.NaT
        else:
            mark_frame = mark_frame.loc[:, ["date", "close"]].copy()
            mark_frame["date"] = pd.to_datetime(mark_frame["date"], utc=True, errors="coerce")
            mark_frame = mark_frame.dropna(subset=["date"]).sort_values("date")
            mark_frame = mark_frame.rename(columns={"close": "mark_close_1h", "date": "mark_source_date_1h"})
            mark_frame["date"] = mark_frame["mark_source_date_1h"]
            merged = pd.merge_asof(merged, mark_frame, on="date", direction="backward")

        funding_frame = load_market_frame("1h", "funding_rate")
        if funding_frame is None or funding_frame.empty:
            merged["funding_rate_1h"] = np.nan
            merged["funding_source_date_1h"] = pd.NaT
        else:
            funding_frame = funding_frame.copy()
            funding_frame["date"] = pd.to_datetime(funding_frame["date"], utc=True, errors="coerce")
            funding_frame = funding_frame.dropna(subset=["date"]).sort_values("date")
            funding_column = _pick_first_populated_column(
                funding_frame,
                ["fundingRate", "funding_rate", "value", "open", "close", "high", "low"],
            )
            if funding_column is None:
                merged["funding_rate_1h"] = np.nan
                merged["funding_source_date_1h"] = pd.NaT
            else:
                funding_frame = funding_frame.loc[:, ["date", funding_column]].rename(
                    columns={funding_column: "funding_rate_1h", "date": "funding_source_date_1h"}
                )
                funding_frame["date"] = funding_frame["funding_source_date_1h"]
                merged = pd.merge_asof(merged, funding_frame, on="date", direction="backward")

        merged["%-mark_available_1h"] = merged["mark_close_1h"].notna().astype(float)
        merged["%-funding_available_1h"] = merged["funding_rate_1h"].notna().astype(float)
        merged["mark_close_1h"] = merged["mark_close_1h"].ffill()
        merged["funding_rate_1h"] = merged["funding_rate_1h"].ffill()
        merged["mark_source_date_1h"] = pd.to_datetime(merged["mark_source_date_1h"], utc=True, errors="coerce").ffill()
        merged["funding_source_date_1h"] = pd.to_datetime(
            merged["funding_source_date_1h"], utc=True, errors="coerce"
        ).ffill()

        merged["%-basis_vs_mark_1h"] = (_safe_div(merged["close"], merged["mark_close_1h"]) - 1.0).fillna(0.0)
        merged["%-mark_return_1h"] = merged["mark_close_1h"].pct_change().fillna(0.0)
        merged["%-funding_rate_1h"] = merged["funding_rate_1h"].fillna(0.0)
        merged["%-funding_rate_delta_1h"] = merged["funding_rate_1h"].diff(1).fillna(0.0)
        merged["%-funding_rate_abs_1h"] = merged["funding_rate_1h"].abs().fillna(0.0)
        merged["%-mark_age_hours_1h"] = (
            (pd.to_datetime(merged["date"], utc=True, errors="coerce") - merged["mark_source_date_1h"])
            .dt.total_seconds()
            .div(3600.0)
            .clip(lower=0.0, upper=24.0)
            .fillna(24.0)
        )
        merged["%-funding_age_hours_1h"] = (
            (pd.to_datetime(merged["date"], utc=True, errors="coerce") - merged["funding_source_date_1h"])
            .dt.total_seconds()
            .div(3600.0)
            .clip(lower=0.0, upper=24.0)
            .fillna(24.0)
        )
        merged["%-mark_available_5m"] = merged["mark_open_5m"].notna().astype(float)
        merged["mark_source_date_5m"] = pd.to_datetime(merged["mark_source_date_5m"], utc=True, errors="coerce").ffill()
        merged["mark_open_5m"] = pd.to_numeric(merged["mark_open_5m"], errors="coerce").ffill()
        merged["mark_close_5m"] = pd.to_numeric(merged["mark_close_5m"], errors="coerce").ffill()
        merged["%-mark_age_minutes_5m"] = (
            (pd.to_datetime(merged["date"], utc=True, errors="coerce") - merged["mark_source_date_5m"])
            .dt.total_seconds()
            .div(60.0)
            .clip(lower=0.0, upper=30.0)
            .fillna(30.0)
        )
        merged["%-mark_freshness_5m"] = (1.0 - (merged["%-mark_age_minutes_5m"] / 30.0)).clip(lower=0.0, upper=1.0)
        merged["%-mark_freshness_1h"] = (1.0 - (merged["%-mark_age_hours_1h"] / 24.0)).clip(lower=0.0, upper=1.0)
        merged["%-funding_freshness_1h"] = (
            1.0 - (merged["%-funding_age_hours_1h"] / 24.0)
        ).clip(lower=0.0, upper=1.0)
        merged["%-basis_vs_mark_effective_1h"] = (
            merged["%-basis_vs_mark_1h"] * merged["%-mark_freshness_1h"]
        ).fillna(0.0)
        merged["%-funding_rate_delta_effective_1h"] = (
            merged["%-funding_rate_delta_1h"] * merged["%-funding_freshness_1h"]
        ).fillna(0.0)
        date_index = pd.to_datetime(merged["date"], utc=True, errors="coerce")
        minute_mod_5m = (date_index.dt.minute % 5).fillna(0).astype(float)
        merged["%-cycle_progress_5m"] = (minute_mod_5m / 5.0).clip(lower=0.0, upper=0.8)
        merged["%-minutes_to_close_5m"] = (5.0 - minute_mod_5m).clip(lower=1.0, upper=5.0)
        merged["bucket_start_5m"] = date_index.dt.floor("5min")
        merged["bucket_start_epoch_5m"] = (
            (merged["bucket_start_5m"].astype("int64") // 10**9)
            .where(merged["bucket_start_5m"].notna(), pd.NA)
            .astype("Int64")
        )
        price_to_beat_lookup = _polymarket_price_to_beat_lookup()
        merged["polymarket_price_to_beat_5m_actual"] = (
            merged["bucket_start_epoch_5m"].map(price_to_beat_lookup).astype(float)
            if price_to_beat_lookup
            else np.nan
        )
        observed_proxy_offset = (
            pd.to_numeric(merged["polymarket_price_to_beat_5m_actual"], errors="coerce")
            - pd.to_numeric(merged["mark_open_5m"], errors="coerce")
        )
        valid_offsets = observed_proxy_offset.replace([np.inf, -np.inf], np.nan).dropna()
        global_proxy_offset = (
            float(valid_offsets.median())
            if not valid_offsets.empty
            else DEFAULT_PRICE_TO_BEAT_PROXY_OFFSET_USD
        )
        rolling_proxy_offset = (
            observed_proxy_offset.ffill().rolling(240, min_periods=1).median().fillna(global_proxy_offset)
        )
        mark_open_anchor = pd.to_numeric(merged["mark_open_5m"], errors="coerce")
        merged["polymarket_price_to_beat_proxy_5m"] = (
            pd.to_numeric(merged["polymarket_price_to_beat_5m_actual"], errors="coerce")
            .fillna(mark_open_anchor + rolling_proxy_offset)
            .fillna(pd.to_numeric(merged["close"], errors="coerce") + global_proxy_offset)
        )
        merged["%-price_to_beat_proxy_delta_1m"] = (
            _safe_div(pd.to_numeric(merged["close"], errors="coerce"), merged["polymarket_price_to_beat_proxy_5m"]) - 1.0
        ).fillna(0.0)
        merged["%-price_to_beat_proxy_offset_5m"] = (
            _safe_div(
                merged["polymarket_price_to_beat_proxy_5m"] - mark_open_anchor,
                merged["polymarket_price_to_beat_proxy_5m"],
            )
        ).fillna(0.0)
        merged["%-mark_open_gap_5m"] = (
            _safe_div(pd.to_numeric(merged["close"], errors="coerce"), mark_open_anchor) - 1.0
        ).fillna(0.0)
        merged["%-price_to_beat_actual_available_5m"] = merged["polymarket_price_to_beat_5m_actual"].notna().astype(float)

        merged["%%basis_vs_mark_1h"] = merged["%-basis_vs_mark_1h"]
        merged["%%funding_rate_delta_1h"] = merged["%-funding_rate_delta_1h"]
        merged["%%basis_vs_mark_effective_1h"] = merged["%-basis_vs_mark_effective_1h"]
        merged["%%funding_rate_delta_effective_1h"] = merged["%-funding_rate_delta_effective_1h"]
        merged["%%price_to_beat_proxy_delta_1m"] = merged["%-price_to_beat_proxy_delta_1m"]
        merged["%%price_to_beat_proxy_offset_5m"] = merged["%-price_to_beat_proxy_offset_5m"]
        merged["%%mark_open_gap_5m"] = merged["%-mark_open_gap_5m"]
        merged["%%cycle_progress_5m"] = merged["%-cycle_progress_5m"]
        merged["%%minutes_to_close_5m"] = merged["%-minutes_to_close_5m"]
        return merged

    def set_freqai_targets(self, dataframe: DataFrame, metadata: dict, **kwargs) -> DataFrame:
        base_thresholds = {
            2: 0.00030,
            5: 0.00050,
            10: 0.00070,
            15: 0.00090,
        }
        dataframe = dataframe.copy()
        close = dataframe["close"]
        atr_ratio_14 = _safe_div(ta.ATR(dataframe, timeperiod=14), close).fillna(0.0)
        realized_vol_20 = close.pct_change().rolling(20).std(ddof=0).fillna(0.0)
        volatility_context = ((realized_vol_20 * 0.7) + (atr_ratio_14 * 0.3)).fillna(0.0)
        trend_pressure = pd.to_numeric(
            dataframe.get("%-trend_pressure_1m", pd.Series(0.0, index=dataframe.index)),
            errors="coerce",
        ).fillna(0.0)
        micro_momentum_balance = pd.to_numeric(
            dataframe.get("%-micro_momentum_balance_1m", pd.Series(0.0, index=dataframe.index)),
            errors="coerce",
        ).fillna(0.0)
        basis_vs_mark = pd.to_numeric(
            dataframe.get("%-basis_vs_mark_effective_1h", dataframe.get("%-basis_vs_mark_1h", pd.Series(0.0, index=dataframe.index))),
            errors="coerce",
        ).fillna(0.0)
        up_breakout_impulse = pd.to_numeric(
            dataframe.get("%-up_breakout_impulse_1m", pd.Series(0.0, index=dataframe.index)),
            errors="coerce",
        ).fillna(0.0)
        down_breakout_impulse = pd.to_numeric(
            dataframe.get("%-down_breakout_impulse_1m", pd.Series(0.0, index=dataframe.index)),
            errors="coerce",
        ).fillna(0.0)
        breakout_up_20 = pd.to_numeric(
            dataframe.get("%-breakout_up_20_1m", pd.Series(0.0, index=dataframe.index)),
            errors="coerce",
        ).fillna(0.0)
        breakout_down_20 = pd.to_numeric(
            dataframe.get("%-breakout_down_20_1m", pd.Series(0.0, index=dataframe.index)),
            errors="coerce",
        ).fillna(0.0)
        volume_thrust = pd.to_numeric(
            dataframe.get("%-volume_thrust_1m", pd.Series(1.0, index=dataframe.index)),
            errors="coerce",
        ).fillna(1.0)
        price_to_beat_proxy = pd.to_numeric(
            dataframe.get("polymarket_price_to_beat_proxy_5m", dataframe["close"]),
            errors="coerce",
        ).replace(0.0, np.nan)
        price_to_beat_proxy = price_to_beat_proxy.fillna(pd.to_numeric(dataframe["close"], errors="coerce"))
        minutes_to_close_5m = pd.to_numeric(
            dataframe.get("%-minutes_to_close_5m", pd.Series(5.0, index=dataframe.index)),
            errors="coerce",
        ).fillna(5.0)
        price_to_beat_proxy_delta = pd.to_numeric(
            dataframe.get("%-price_to_beat_proxy_delta_1m", pd.Series(0.0, index=dataframe.index)),
            errors="coerce",
        ).fillna(0.0)

        # Normalize current-bar context into a bounded directional bias.
        trend_bias = (
            np.tanh(trend_pressure * 250.0) * 0.50
            + np.tanh(micro_momentum_balance * 1.35) * 0.35
            + np.tanh(basis_vs_mark * 180.0) * 0.15
        ).clip(-1.0, 1.0)
        bullish_impulse = (
            np.tanh(up_breakout_impulse * 1.8) * 0.55
            + np.tanh(breakout_up_20 * 900.0) * 0.20
            + np.tanh((volume_thrust - 1.0) * 1.4) * 0.10
            + trend_bias.clip(lower=0.0) * 0.15
        ).clip(0.0, 1.0)
        bearish_impulse = (
            np.tanh(down_breakout_impulse * 1.8) * 0.55
            + np.tanh((-breakout_down_20) * 900.0) * 0.20
            + np.tanh((volume_thrust - 1.0) * 1.4) * 0.10
            + (-trend_bias).clip(lower=0.0) * 0.15
        ).clip(0.0, 1.0)

        label_columns: dict[str, pd.Series] = {}
        for horizon_minutes, base_threshold in base_thresholds.items():
            horizon_scale = float(np.sqrt(horizon_minutes / 5.0))
            adaptive_threshold = np.maximum(
                base_threshold,
                volatility_context * horizon_scale * 0.45,
            )
            bullish_bias = trend_bias.clip(lower=0.0)
            bearish_bias = (-trend_bias).clip(lower=0.0)
            up_threshold = adaptive_threshold * (
                1.0
                - bullish_bias * 0.08
                - bullish_impulse * 0.14
                + bearish_bias * 0.20
                + bearish_impulse * 0.10
            )
            down_threshold = adaptive_threshold * (
                1.0
                - bearish_bias * 0.08
                - bearish_impulse * 0.12
                + bullish_bias * 0.22
                + bullish_impulse * 0.08
            )
            up_threshold = np.clip(up_threshold, adaptive_threshold * 0.85, adaptive_threshold * 1.35)
            down_threshold = np.clip(down_threshold, adaptive_threshold * 0.85, adaptive_threshold * 1.35)
            cycle_close_pressure = ((5.0 - minutes_to_close_5m).clip(lower=0.0, upper=4.0) / 4.0).fillna(0.0)
            event_alignment = np.where(horizon_minutes <= 5, 1.0, 0.55)
            event_threshold_scale = (
                1.0
                - event_alignment * cycle_close_pressure * 0.08
                - event_alignment * price_to_beat_proxy_delta.abs().clip(upper=0.006) * 10.0
            )
            event_threshold_scale = np.clip(event_threshold_scale, 0.82, 1.10)
            up_threshold = up_threshold * event_threshold_scale
            down_threshold = down_threshold * event_threshold_scale
            future_return = dataframe["close"].shift(-horizon_minutes) / price_to_beat_proxy - 1.0
            label_columns[f"&-dir_{horizon_minutes}m"] = pd.Series(
                np.select(
                [
                        future_return <= -down_threshold,
                        future_return >= up_threshold,
                ],
                [
                    f"DOWN_{horizon_minutes}M",
                    f"UP_{horizon_minutes}M",
                ],
                default=f"NO_EDGE_{horizon_minutes}M",
                ),
                index=dataframe.index,
            )
        return pd.concat([dataframe, pd.DataFrame(label_columns, index=dataframe.index)], axis=1)

    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        dataframe = self.freqai.start(dataframe, metadata, self)
        return dataframe

    @staticmethod
    def _probability(dataframe: DataFrame, column: str) -> pd.Series:
        if column in dataframe.columns:
            return pd.to_numeric(dataframe[column], errors="coerce").fillna(0.0)
        return pd.Series(0.0, index=dataframe.index)

    @staticmethod
    def _helper(dataframe: DataFrame, column: str) -> pd.Series:
        if column in dataframe.columns:
            return pd.to_numeric(dataframe[column], errors="coerce").fillna(0.0)
        return pd.Series(0.0, index=dataframe.index)

    def populate_entry_trend(self, df: DataFrame, metadata: dict) -> DataFrame:
        up_2m = self._probability(df, "UP_2M")
        down_2m = self._probability(df, "DOWN_2M")
        no_edge_2m = self._probability(df, "NO_EDGE_2M")
        up_5m = self._probability(df, "UP_5M")
        down_5m = self._probability(df, "DOWN_5M")
        no_edge_5m = self._probability(df, "NO_EDGE_5M")
        up_10m = self._probability(df, "UP_10M")
        down_10m = self._probability(df, "DOWN_10M")
        up_15m = self._probability(df, "UP_15M")
        down_15m = self._probability(df, "DOWN_15M")

        score_2m = up_2m - down_2m
        score_5m = up_5m - down_5m
        score_10m = up_10m - down_10m
        score_15m = up_15m - down_15m
        horizon_weights = [0.30, 0.40, 0.20, 0.10]
        composite_score = (score_2m * horizon_weights[0]) + (score_5m * horizon_weights[1]) + (score_10m * horizon_weights[2]) + (score_15m * horizon_weights[3])
        directional_probability = pd.concat([up_2m, up_5m, up_10m, up_15m], axis=1).mul(horizon_weights, axis=1).sum(axis=1)
        directional_probability = directional_probability.where(composite_score >= 0.0, pd.concat([down_2m, down_5m, down_10m, down_15m], axis=1).mul(horizon_weights, axis=1).sum(axis=1))

        rsi_1m = self._helper(df, "%%rsi_1m")
        macd_slope = self._helper(df, "%%macd_hist_slope_1m")
        vwap_distance = self._helper(df, "%%vwap_distance_1m")
        donchian_breakout = self._helper(df, "%%donchian_breakout_3m")
        ema_gap_5_20 = self._helper(df, "%%ema_gap_5_20")
        ema_gap_10_50 = self._helper(df, "%%ema_gap_10_50")
        basis_vs_mark = self._helper(df, "%%basis_vs_mark_effective_1h")
        funding_delta = self._helper(df, "%%funding_rate_delta_effective_1h")
        no_edge_10m = self._probability(df, "NO_EDGE_10M")
        no_edge_15m = self._probability(df, "NO_EDGE_15M")

        bullish_momentum_votes = (
            (rsi_1m >= 0.52).astype(int)
            + (macd_slope > 0.0).astype(int)
            + (vwap_distance > 0.0).astype(int)
            + (donchian_breakout > 0.0).astype(int)
        )
        bearish_momentum_votes = (
            (rsi_1m <= 0.48).astype(int)
            + (macd_slope < 0.0).astype(int)
            + (vwap_distance < 0.0).astype(int)
            + (donchian_breakout < 0.0).astype(int)
        )
        bullish_short_guard = (
            (bullish_momentum_votes >= 3)
            | ((rsi_1m >= 0.54) & (ema_gap_5_20 >= 0.0008) & (ema_gap_10_50 >= 0.0020))
            | ((basis_vs_mark >= 0.0025) & (ema_gap_10_50 >= 0.0020))
        )
        bearish_long_guard = (
            (bearish_momentum_votes >= 3)
            | ((rsi_1m <= 0.46) & (ema_gap_5_20 <= -0.0008) & (ema_gap_10_50 <= -0.0020))
            | ((basis_vs_mark <= -0.0025) & (ema_gap_10_50 <= -0.0020))
        )
        long_supportive_horizons = (
            (up_10m >= down_10m + 0.02).astype(int)
            + (up_15m >= down_15m + 0.02).astype(int)
        )
        short_supportive_horizons = (
            (down_10m >= up_10m + 0.02).astype(int)
            + (down_15m >= up_15m + 0.02).astype(int)
        )
        weighted_no_edge = (no_edge_2m * horizon_weights[0]) + (no_edge_5m * horizon_weights[1]) + (no_edge_10m * horizon_weights[2]) + (no_edge_15m * horizon_weights[3])

        long_conditions = [
            df["do_predict"] == 1,
            composite_score > 0.18,
            directional_probability >= 0.50,
            up_5m >= 0.50,
            up_2m + 0.08 >= down_2m,
            weighted_no_edge <= 0.30,
            no_edge_5m <= 0.35,
            bullish_momentum_votes >= 2,
            bearish_momentum_votes <= 1,
            long_supportive_horizons >= 1,
            ~bearish_long_guard,
            ema_gap_5_20 >= -0.0002,
            ema_gap_10_50 >= -0.0012,
            basis_vs_mark >= -0.0040,
            funding_delta >= -0.00035,
            no_edge_2m <= 0.45,
            df["volume"] > 0,
        ]
        short_conditions = [
            df["do_predict"] == 1,
            composite_score < -0.18,
            directional_probability >= 0.50,
            down_5m >= 0.50,
            down_2m + 0.08 >= up_2m,
            weighted_no_edge <= 0.30,
            no_edge_5m <= 0.35,
            bearish_momentum_votes >= 2,
            bullish_momentum_votes <= 1,
            short_supportive_horizons >= 1,
            ~bullish_short_guard,
            ema_gap_5_20 <= 0.0002,
            ema_gap_10_50 <= 0.0012,
            basis_vs_mark <= 0.0040,
            funding_delta <= 0.00035,
            no_edge_2m <= 0.45,
            df["volume"] > 0,
        ]

        if long_conditions:
            df.loc[
                np.logical_and.reduce(long_conditions),
                ["enter_long", "enter_tag"],
            ] = (1, "freqai_btc_short_horizon_long")

        if short_conditions:
            df.loc[
                np.logical_and.reduce(short_conditions),
                ["enter_short", "enter_tag"],
            ] = (1, "freqai_btc_short_horizon_short")

        return df

    def populate_exit_trend(self, df: DataFrame, metadata: dict) -> DataFrame:
        up_2m = self._probability(df, "UP_2M")
        down_2m = self._probability(df, "DOWN_2M")
        no_edge_2m = self._probability(df, "NO_EDGE_2M")
        up_5m = self._probability(df, "UP_5M")
        down_5m = self._probability(df, "DOWN_5M")
        no_edge_5m = self._probability(df, "NO_EDGE_5M")
        up_10m = self._probability(df, "UP_10M")
        down_10m = self._probability(df, "DOWN_10M")
        up_15m = self._probability(df, "UP_15M")
        down_15m = self._probability(df, "DOWN_15M")

        score_2m = up_2m - down_2m
        score_5m = up_5m - down_5m
        score_10m = up_10m - down_10m
        score_15m = up_15m - down_15m
        composite_score = (score_2m * 0.35) + (score_5m * 0.40) + (score_10m * 0.15) + (score_15m * 0.10)

        rsi_1m = self._helper(df, "%%rsi_1m")
        macd_slope = self._helper(df, "%%macd_hist_slope_1m")
        vwap_distance = self._helper(df, "%%vwap_distance_1m")
        no_edge_pressure = (no_edge_2m * 0.45) + (no_edge_5m * 0.55)

        exit_long_conditions = [
            (composite_score < -0.14)
            | ((score_2m < -0.16) & (score_5m < -0.05))
            | ((rsi_1m < 0.47) & (macd_slope < 0.0) & (vwap_distance < 0.0) & (no_edge_pressure > 0.48))
        ]
        exit_short_conditions = [
            (composite_score > 0.14)
            | ((score_2m > 0.16) & (score_5m > 0.05))
            | ((rsi_1m > 0.53) & (macd_slope > 0.0) & (vwap_distance > 0.0) & (no_edge_pressure > 0.48))
        ]

        df.loc[np.logical_and.reduce(exit_long_conditions), ["exit_long", "exit_tag"]] = (
            1,
            "freqai_btc_short_horizon_exit_long",
        )
        df.loc[np.logical_and.reduce(exit_short_conditions), ["exit_short", "exit_tag"]] = (
            1,
            "freqai_btc_short_horizon_exit_short",
        )
        return df

    def custom_exit(
        self,
        pair: str,
        trade: Trade,
        current_time: datetime,
        current_rate: float,
        current_profit: float,
        **kwargs,
    ):
        if current_time - trade.open_date_utc >= timedelta(minutes=15):
            return "horizon_timeout_15m"
        if current_profit >= 0.009:
            return "fast_take_profit"
        return None
