# source: https://raw.githubusercontent.com/AlexEisie/GeniusTraderTaffy/782b201c5a48e7b21748de9d631ae9f0ef369154/strategies/old/VolatilityBreakout.py
# directory_url: https://github.com/AlexEisie/GeniusTraderTaffy/blob/main/strategies/old/
# User: AlexEisie
# Repository: GeniusTraderTaffy
# --------------------"""
Github_AlexEisie_GeniusTraderTaffy__VolatilityBreakout__20260819_144029 —— Larry Williams 波动率突破策略的 freqtrade 移植版
====================================================================
来源：Larry Williams 的 Volatility Break-out（《短线交易秘诀》）
      加密圈流行的版本来自韩国社区（"변동성 돌파 전략"），
      参考实现 https://github.com/sharebook-kr/larry_simple

原始规则（完全外部定义，本文件只做移植，不做原创改动）：
    range  = 前一日最高价 - 前一日最低价
    target = 当日开盘价 + K × range          （K 通常取 0.5）
    入场   = 当日价格向上突破 target 时买入
    出场   = 当日收盘（≈次日开盘）无条件平仓
    过滤   = 当日开盘价需在 5 日均线上方（噪音过滤，社区标准加法）

移植时做的三处工程性调整（都已尽量保持中性，不改变策略性质）：
  1. 执行粒度用 15m K 线判断突破。原策略在实盘中是 tick 级触发，
     15m 会让实际成交价略高于 target，这是真实的执行成本，没有美化。
  2. "前一日高低" 通过 1d informative 取得。freqtrade 的
     merge_informative_pair 会自动做时间对齐，不存在未来函数。
     "当日开盘价" 由 15m 数据自行分组计算，同样只用已知信息。
  3. 原策略没有止损（当日平仓就是风控）。freqtrade 必须设 stoploss，
     这里设 -0.08 作为断路器。**收紧这个值会改变策略性质**，
     想忠实复现就别动它。

注意：这个策略在 2017-2021 年的加密市场表现很好，此后边缘明显衰减。
必须自己在近期数据上验证，不要采信任何旧的收益截图。
====================================================================
"""

import talib.abstract as ta
from pandas import DataFrame

from freqtrade.strategy import DecimalParameter, IStrategy, informative


class Github_AlexEisie_GeniusTraderTaffy__VolatilityBreakout__20260819_144029(IStrategy):
    INTERFACE_VERSION = 3

    timeframe = "15m"
    can_short = False

    # 原策略靠「当日平仓」控制风险，不用 ROI 止盈。
    # 设成 10（1000%）等于关闭 ROI 出场。
    minimal_roi = {"0": 10}

    # 原策略无止损，这里仅作断路器
    stoploss = -0.08
    trailing_stop = False
    use_custom_stoploss = False

    process_only_new_candles = True
    use_exit_signal = True
    exit_profit_only = False
    ignore_roi_if_entry_signal = False

    # 需要 5 根日线做均线过滤：5 × 96 根 15m = 480，留足缓冲
    startup_candle_count = 600

    # ---------------- 唯一的核心参数 ----------------
    # K 越小越容易触发（交易更多、噪音更多），越大越挑剔
    k_value = DecimalParameter(0.2, 0.9, default=0.5, decimals=2, space="buy", optimize=True)

    @property
    def protections(self):
        return [
            # 平仓后冷却 2 小时，避免同一天内被反复扫进扫出
            {"method": "CooldownPeriod", "stop_duration_candles": 8},
        ]

    # ---------------- 日线指标：前一日高低 + 5 日均线 ----------------
    @informative("1d")
    def populate_indicators_1d(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        # 这里的 high / low / close 都是「已收盘的日线」，
        # merge 时 freqtrade 会自动错位对齐，取到的就是前一日的值
        dataframe["ma5"] = ta.SMA(dataframe, timeperiod=5)
        return dataframe

    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        # 前一日振幅
        dataframe["prev_range"] = dataframe["high_1d"] - dataframe["low_1d"]

        # 当日开盘价：按 UTC 自然日分组取第一根 15m 的 open
        # transform("first") 只回填组内第一个值，不涉及未来数据
        day = dataframe["date"].dt.floor("1D")
        dataframe["day_start"] = day
        dataframe["day_open"] = dataframe.groupby(day)["open"].transform("first")

        # 突破目标价
        dataframe["target"] = dataframe["day_open"] + dataframe["prev_range"] * self.k_value.value

        return dataframe

    # ---------------- 入场：当日首次向上突破 target ----------------
    def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        crossed = (
            (dataframe["close"] > dataframe["target"])
            & (dataframe["close"].shift(1) <= dataframe["target"].shift(1))
            & (dataframe["prev_range"] > 0)
            & (dataframe["volume"] > 0)
        )

        # 噪音过滤：当日开盘价站在 5 日均线上方才允许做多
        trend_ok = dataframe["day_open"] > dataframe["ma5_1d"]

        # 每天只取第一次突破，避免当日反复触发
        first_of_day = crossed.groupby(dataframe["day_start"]).cumsum() == 1

        dataframe.loc[
            crossed & trend_ok & first_of_day,
            ["enter_long", "enter_tag"],
        ] = (1, "vb_breakout")

        return dataframe

    # ---------------- 出场：当日最后一根 15m K 线无条件平仓 ----------------
    def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        dataframe.loc[
            (dataframe["date"].dt.hour == 23)
            & (dataframe["date"].dt.minute >= 45)
            & (dataframe["volume"] > 0),
            ["exit_long", "exit_tag"],
        ] = (1, "day_close")

        return dataframe

    plot_config = {
        "main_plot": {
            "target": {"color": "red"},
            "day_open": {"color": "grey"},
            "ma5_1d": {"color": "orange"},
        },
    }
