# structure_flow_momentum_scalp.py
# 顺趋势剥头皮策略 v2.0
#
# 核心思路：不再在S/R处做反向交易接飞刀，而是顺趋势方向，等回调后入场。
#
# ┌─────────────────────────────────────────────────────────────┐
# │  15m趋势方向判断（EMA20 vs EMA50）                           │
# │         ↓                                                    │
# │  上升趋势 → 只等5m回调到EMA20/支撑附近 → 止跌信号 → 做多     │
# │  下降趋势 → 只等5m反弹到EMA20/阻力附近 → 止涨信号 → 做空     │
# │         ↓                                                    │
# │  止损：ATR×1.0 | 止盈：ATR×1.5 | 时间止损：60分钟           │
# └─────────────────────────────────────────────────────────────┘
#
# v2.0 (2026-06-10): 初始版本，完全重写

from freqtrade.strategy import IStrategy, IntParameter, DecimalParameter, informative
from pandas import DataFrame
import pandas as pd
import numpy as np
from datetime import datetime
from freqtrade.persistence import Trade


class StructureFlowMomentumScalp(IStrategy):
    """
    顺趋势剥头皮策略 v2.0

    核心逻辑：
    - 15m EMA趋势方向过滤，只做顺趋势方向的单
    - 5m 回调到EMA20或S/R支撑/阻力区域时，等待K线信号确认后入场
    - 止损 ATR×1.0，止盈 ATR×1.5，时间止损 60 分钟
    - 不做方向猜测，不吃鱼头鱼尾，只吃回调结束那一小段
    """

    # ── 时间框架 ──
    timeframe = "5m"

    # ── 交易参数 ──
    can_short = True
    max_open_trades = 1
    stake_amount = "unlimited"
    use_custom_stoploss = True
    use_exit_signal = False  # 出场完全由 custom_stoploss + custom_exit 管理

    # ── 合约参数 ──
    margin_mode = "cross"
    trading_mode = "futures"

    # ── 可优化参数 ──
    # 趋势检测
    trend_ema_period = IntParameter(10, 30, default=20, space="buy")
    # 回调确认幅度
    pullback_deviation = DecimalParameter(0.2, 1.0, default=0.5, decimals=1, space="buy")
    # 入场冷却期
    cooldown_bars = IntParameter(2, 8, default=3, space="buy")
    # K线形态灵敏度
    pin_bar_wick_ratio = IntParameter(50, 80, default=60, space="buy")
    # 止损ATR倍数
    atr_mult_stop = DecimalParameter(0.8, 2.0, default=1.0, decimals=1, space="sell")
    # 止盈ATR倍数
    atr_mult_tp = DecimalParameter(1.0, 3.0, default=1.5, decimals=1, space="sell")

    # ── 常数 ──
    time_stop_minutes = 60  # 最大持仓时间

    # ── 保护性止损 ──
    stoploss = -0.10  # 硬止损 10%

    # ================================================================
    #  杠杆
    # ================================================================

    def leverage(
        self, pair: str, current_time: datetime, current_rate: float,
        proposed_leverage: float, max_leverage: float, side: str,
        **kwargs,
    ) -> float:
        """20x 杠杆起步，验证胜率后再上量"""
        return min(20.0, max_leverage)

    # ================================================================
    #  信息时间框架 — 15m 趋势判断 + S/R
    # ================================================================

    @informative("15m")
    def populate_indicators_15m(
        self, dataframe: DataFrame, metadata: dict
    ) -> DataFrame:
        """15m级别：EMA趋势方向 + swing point S/R。"""

        # ── EMA 趋势方向 ──
        ema_period = self.trend_ema_period.value
        dataframe["ema_fast"] = dataframe["close"].ewm(span=ema_period, adjust=False).mean()
        dataframe["ema_slow"] = dataframe["close"].ewm(span=ema_period * 2.5, adjust=False).mean()

        dataframe["trend_up"] = dataframe["ema_fast"] > dataframe["ema_slow"]
        dataframe["trend_down"] = dataframe["ema_fast"] < dataframe["ema_slow"]

        # ── Swing Point 支撑/阻力 ──
        high = dataframe["high"].tolist()
        low = dataframe["low"].tolist()
        close = dataframe["close"].tolist()

        sh, sl = self._detect_swing_points(high, low, window=5)
        trend_up_arr, trend_down_arr, support_arr, resistance_arr = self._build_structure(
            high, low, close, sh, sl,
        )

        dataframe["trend_up_sp"] = trend_up_arr
        dataframe["trend_down_sp"] = trend_down_arr
        # EMA平滑S/R（避免跳变）
        dataframe["support"] = self._ema_smooth(support_arr, alpha=0.3)
        dataframe["resistance"] = self._ema_smooth(resistance_arr, alpha=0.3)

        return dataframe

    # ================================================================
    #  主框架 — 5m 级别指标
    # ================================================================

    def populate_indicators(
        self, dataframe: DataFrame, metadata: dict
    ) -> DataFrame:
        """5m级别：ATR + K线形态 + EMA趋势整合。"""

        # ── ATR(14) ──
        high = dataframe["high"]
        low = dataframe["low"]
        close = dataframe["close"]
        prev_close = close.shift(1)
        tr = pd.concat([
            high - low,
            (high - prev_close).abs(),
            (low - prev_close).abs(),
        ], axis=1).max(axis=1)
        dataframe["atr"] = tr.rolling(14).mean()
        atr_mean = dataframe["atr"].mean()
        dataframe["atr"] = dataframe["atr"].fillna(atr_mean)

        # ── K线形态 ──
        bullish_pin, bearish_pin, bullish_engulf, bearish_engulf = (
            self._detect_candle_patterns(
                dataframe["open"], dataframe["high"], dataframe["low"],
                dataframe["close"], self.pin_bar_wick_ratio.value / 100.0,
            )
        )
        dataframe["bullish_signal"] = bullish_pin | bullish_engulf
        dataframe["bearish_signal"] = bearish_pin | bearish_engulf

        # ── 5m EMA（用于短期拉回确认） ──
        dataframe["ema5"] = close.ewm(span=5, adjust=False).mean()
        dataframe["ema8"] = close.ewm(span=8, adjust=False).mean()

        # ── 布尔列NaN填充 ──
        for col in ["bullish_signal", "bearish_signal"]:
            dataframe[col] = dataframe[col].fillna(False)

        return dataframe

    # ================================================================
    #  入场逻辑
    # ================================================================

    def populate_entry_trend(
        self, dataframe: DataFrame, metadata: dict
    ) -> DataFrame:
        """
        入场逻辑。

        只做顺趋势回调入场，不做S/R反向交易：

        做多条件：
        1. 15m 上升趋势（EMA_fast > EMA_slow）
        2. 5m 价格回调到15m EMA_fast 或 支撑位附近
        3. 5m K线止跌信号（pinbar/engulfing）

        做空条件（对称）：
        1. 15m 下降趋势
        2. 5m 价格反弹到15m EMA_fast 或 阻力位附近
        3. 5m K线止涨信号
        """
        cooldown = self.cooldown_bars.value
        dev = self.pullback_deviation.value / 100.0  # 0.5% → 0.005

        # ── 必要列检查 ──
        required = [
            "ema_fast_15m", "trend_up_15m", "trend_down_15m",
            "support_15m", "resistance_15m",
        ]
        for col in required:
            if col not in dataframe.columns:
                return dataframe

        # ── 布尔列填充 ──
        for col in [
            "bullish_signal", "bearish_signal",
            "trend_up_15m", "trend_down_15m",
        ]:
            if col in dataframe.columns:
                dataframe[col] = dataframe[col].fillna(False)

        # ═══════════════════════════════════════════════════════════
        #  做多：上升趋势 + 回调到EMA/支撑 + 止跌信号
        # ═══════════════════════════════════════════════════════════

        # 条件1：15m 上升趋势
        trend_up = dataframe["trend_up_15m"]

        # 条件2：价格在EMA20或支撑位附近（回调到顺趋势的支撑区）
        near_ema = (
            (dataframe["low"] <= dataframe["ema_fast_15m"] * (1.0 + dev * 0.5)) &
            (dataframe["low"] >= dataframe["ema_fast_15m"] * (1.0 - dev * 2.0))
        )
        near_support = (
            (dataframe["low"] <= dataframe["support_15m"] * (1.0 + dev)) &
            (dataframe["low"] >= dataframe["support_15m"] * (1.0 - dev))
        )
        pullback_long = near_ema | near_support

        # 条件3：K线止跌信号
        signal_long = dataframe["bullish_signal"]

        # 综合入场
        enter_long = trend_up & pullback_long & signal_long
        long_recent = enter_long.rolling(cooldown, min_periods=1).max().shift(1) == 0
        dataframe.loc[enter_long & long_recent, "enter_long"] = 1

        # ═══════════════════════════════════════════════════════════
        #  做空：下降趋势 + 反弹到EMA/阻力 + 止涨信号
        # ═══════════════════════════════════════════════════════════

        # 条件1：15m 下降趋势
        trend_down = dataframe["trend_down_15m"]

        # 条件2：价格在EMA20或阻力位附近（反弹到顺趋势的阻力区）
        near_ema_short = (
            (dataframe["high"] >= dataframe["ema_fast_15m"] * (1.0 - dev * 0.5)) &
            (dataframe["high"] <= dataframe["ema_fast_15m"] * (1.0 + dev * 2.0))
        )
        near_resistance = (
            (dataframe["high"] >= dataframe["resistance_15m"] * (1.0 - dev)) &
            (dataframe["high"] <= dataframe["resistance_15m"] * (1.0 + dev))
        )
        pullback_short = near_ema_short | near_resistance

        # 条件3：K线止涨信号
        signal_short = dataframe["bearish_signal"]

        # 综合入场
        enter_short = trend_down & pullback_short & signal_short
        short_recent = enter_short.rolling(cooldown, min_periods=1).max().shift(1) == 0
        dataframe.loc[enter_short & short_recent, "enter_short"] = 1

        return dataframe

    # ================================================================
    #  exit_trend（freqtrade 2025.11 强制要求，即使 use_exit_signal=False）
    # ================================================================

    def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        """出场完全由 custom_stoploss + custom_exit 管理。"""
        return dataframe

    # ================================================================
    #  出场 — 止损（ATR动态）
    # ================================================================

    def custom_stoploss(
        self,
        pair: str,
        trade: Trade,
        current_time: datetime,
        current_rate: float,
        current_profit: float,
        after_fill: bool,
        **kwargs,
    ) -> float:
        """
        止损 = 入场价 ± ATR × atr_mult_stop

        - ATR值从入场K线锁定，持仓期间不变
        - 做多：entry_price - (locked_atr × mult)
        - 做空：entry_price + (locked_atr × mult)
        - 配20x杠杆，ATR×1.0 ≈ 对应约 $3.7 止损（当前5m ATR~$3.74）
        """
        dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
        if dataframe is None or len(dataframe) == 0:
            return -0.02 if not trade.is_short else 0.02

        entry_row = self._get_entry_row(dataframe, trade)
        if entry_row is None:
            return -0.02 if not trade.is_short else 0.02

        atr = entry_row.get("atr", np.nan)
        if pd.isna(atr) or atr <= 0:
            return -0.02 if not trade.is_short else 0.02

        mult = self.atr_mult_stop.value

        if not trade.is_short:
            sl_price = trade.open_rate - (atr * mult)
            sl_ratio = (sl_price / trade.open_rate) - 1.0
            return max(sl_ratio, -self.stoploss)
        else:
            sl_price = trade.open_rate + (atr * mult)
            sl_ratio = 1.0 - (sl_price / trade.open_rate)
            return min(sl_ratio, self.stoploss)

    # ================================================================
    #  出场 — 止盈（ATR动态）+ 时间止损
    # ================================================================

    def custom_exit(
        self,
        pair: str,
        trade: Trade,
        current_time: datetime,
        current_rate: float,
        current_profit: float,
        **kwargs,
    ) -> str | None:
        """
        出场逻辑：
        1. ATR止盈：利润达到入场时锁定的 ATR × atr_mult_tp → 止盈
        2. 时间止损：持仓超过 time_stop_minutes → 强制出场
        """
        dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
        if dataframe is None or len(dataframe) == 0:
            return None

        entry_row = self._get_entry_row(dataframe, trade)
        if entry_row is None:
            return None

        atr = entry_row.get("atr", np.nan)
        if pd.isna(atr) or atr <= 0:
            return None

        # 1. ATR 止盈
        tp_mult = self.atr_mult_tp.value
        tp_ratio = (atr * tp_mult) / trade.open_rate

        if current_profit >= tp_ratio:
            return "atr_tp"

        # 2. 时间止损
        elapsed = (current_time - trade.open_date).total_seconds() / 60.0
        if elapsed >= self.time_stop_minutes:
            return "time_stop"

        return None

    # ================================================================
    #  工具函数
    # ================================================================

    def _detect_swing_points(
        self, highs: list, lows: list, window: int = 5
    ):
        """
        Swing High / Swing Low 检测。

        当一根K线的最高价高于其两侧window根K线的最高价时，标记为Swing High。
        Swing Low同理。
        """
        n = len(highs)
        swing_high = [np.nan] * n
        swing_low = [np.nan] * n

        for i in range(window, n - window):
            # Swing High
            is_high = True
            for j in range(i - window, i + window + 1):
                if j == i:
                    continue
                if highs[j] >= highs[i]:
                    is_high = False
                    break
            if is_high:
                swing_high[i] = highs[i]

            # Swing Low
            is_low = True
            for j in range(i - window, i + window + 1):
                if j == i:
                    continue
                if lows[j] <= lows[i]:
                    is_low = False
                    break
            if is_low:
                swing_low[i] = lows[i]

        return swing_high, swing_low

    def _build_structure(
        self, highs: list, lows: list, closes: list,
        swing_high: list, swing_low: list,
    ):
        """构建趋势结构和支撑/阻力位。"""
        n = len(highs)
        trend_up = [False] * n
        trend_down = [False] * n
        support = [np.nan] * n
        resistance = [np.nan] * n

        # 用最近4个swing point的位置判断
        last_sh_idx = -1
        last_sl_idx = -1
        prev_sh = []
        prev_sl = []

        for i in range(n):
            if not np.isnan(swing_high[i]):
                prev_sh.append(swing_high[i])
                last_sh_idx = i
                if len(prev_sh) > 4:
                    prev_sh.pop(0)

            if not np.isnan(swing_low[i]):
                prev_sl.append(swing_low[i])
                last_sl_idx = i
                if len(prev_sl) > 4:
                    prev_sl.pop(0)

            # 趋势判断：最新的HH > 次新的HH = 上升趋势中的higher high
            if len(prev_sh) >= 2 and prev_sh[-1] > prev_sh[-2]:
                trend_up[i] = True

            # 趋势判断：最新的LL < 次新的LL = 下降趋势中的lower low
            if len(prev_sl) >= 2 and prev_sl[-1] < prev_sl[-2]:
                trend_down[i] = True

            # 支撑 = 最近的有效Swing Low（EMA平滑后在调用侧处理）
            if prev_sl:
                support[i] = prev_sl[-1]
            if prev_sh:
                resistance[i] = prev_sh[-1]

        return trend_up, trend_down, support, resistance

    def _ema_smooth(self, values: list, alpha: float = 0.3):
        """对数组做EMA平滑，避免跳变。"""
        result = [np.nan] * len(values)
        ema = None
        for i, v in enumerate(values):
            if pd.isna(v) or v is None:
                if ema is not None:
                    result[i] = ema
                continue
            if ema is None:
                ema = v
            else:
                ema = alpha * v + (1 - alpha) * ema
            result[i] = ema
        return np.array(result)

    def _detect_candle_patterns(
        self, opens, highs, lows, closes, wick_ratio=0.6,
    ):
        """检测K线形态：pinbar（锤子线/射击星）和吞没形态。"""
        n = len(opens)
        bullish_pin = [False] * n
        bearish_pin = [False] * n
        bullish_engulf = [False] * n
        bearish_engulf = [False] * n

        for i in range(n):
            o, h, l, c = opens[i], highs[i], lows[i], closes[i]
            total_range = h - l if h > l else 0.001

            is_bullish = c > o
            is_bearish = c < o

            body = abs(c - o)
            upper_wick = h - max(c, o)
            lower_wick = min(c, o) - l

            # Pinbar：影线 > total_range × wick_ratio
            if is_bullish and lower_wick / total_range > wick_ratio:
                bullish_pin[i] = True
            if is_bearish and upper_wick / total_range > wick_ratio:
                bearish_pin[i] = True

            # 吞没形态
            if i > 0:
                prev_o = opens[i - 1]
                prev_c = closes[i - 1]
                if is_bullish and c > prev_o and o < prev_c:
                    bullish_engulf[i] = True
                if is_bearish and c < prev_o and o > prev_c:
                    bearish_engulf[i] = True

        return (
            pd.Series(bullish_pin),
            pd.Series(bearish_pin),
            pd.Series(bullish_engulf),
            pd.Series(bearish_engulf),
        )

    def _get_entry_row(self, dataframe: DataFrame, trade: Trade):
        """查找入场K线行，兼容live/backtesting两种模式。"""
        if "date" in dataframe.columns:
            entry_mask = pd.to_datetime(dataframe["date"]) <= trade.open_date
            if not entry_mask.any():
                return None
            return dataframe[entry_mask].iloc[-1]
        else:
            try:
                idx = dataframe.index.get_indexer([trade.open_date], method="pad")
                if idx[0] < 0 or idx[0] >= len(dataframe):
                    return None
                return dataframe.iloc[idx[0]]
            except (TypeError, ValueError):
                return None
