# source: https://raw.githubusercontent.com/AlexEisie/GeniusTraderTaffy/782b201c5a48e7b21748de9d631ae9f0ef369154/strategies/old/AdaptiveTrend.py
# directory_url: https://github.com/AlexEisie/GeniusTraderTaffy/blob/main/strategies/old/
# User: AlexEisie
# Repository: GeniusTraderTaffy
# --------------------"""
Github_AlexEisie_GeniusTraderTaffy__AdaptiveTrend__20260819_144029 —— 论文趋势跟踪机制的 freqtrade 移植版
====================================================================
来源：arXiv 2602.11708
      "Systematic Trend-Following with Adaptive Portfolio Construction:
       Enhancing Risk-Adjusted Alpha in Cryptocurrency Markets" (2026-02)

论文原始规则（单资产部分，本文件忠实移植）：
    时间周期  6h
    动量      MOM_t = (P_t - P_{t-L}) / P_{t-L}
    入场      MOM > θ_entry 做多；MOM < -θ_entry 做空
    移动止损  S_t = max(S_{t-1}, P_t - α × ATR_t)，α = 2.5（论文最优区间 2.0~3.5）
    出场      价格跌破移动止损，除此之外没有任何止盈

--------------------------------------------------------------------
我丢掉了论文的哪些部分，以及为什么
--------------------------------------------------------------------
1. **每月网格搜索重优化 L 和 θ_entry** —— 丢弃。
   论文自己在 Limitations 里写了 "Monthly reoptimization introduces
   look-ahead bias risk"。它报告的 Sharpe 2.41 / 回撤 12.7% 极可能
   来自这个环节。这里把 L 和 θ 固定死，谁也别想偷看未来。
   **代价：实际表现一定比论文数字差很多。这是诚实的代价。**

2. **按市值取前 15 做多 + 上月 Sharpe ≥ 1.3 过滤** —— 丢弃。
   freqtrade 拿不到市值数据；"上月夏普高的才买"是典型的追逐近期
   表现，很容易在回测里制造假象。改用你自己的静态币对列表。

3. **70/30 多空配比** —— 丢弃。现货只能做多。
   文件里保留了做空逻辑，切到合约时把 can_short 改成 True 即可。

--------------------------------------------------------------------
一处需要你知道的解释性选择
--------------------------------------------------------------------
论文写的是 "Long entry when momentum exceeds threshold"，字面意思是
只要 MOM > θ 就持有。这样实现的话，止损出场后只要动量还在阈值上方
就会立刻重新入场，容易反复被扫。
这里改成**动量向上穿越阈值的那一根 K 线才入场**。想换回字面版本，
把 populate_entry_trend 里的 `& (mom.shift(1) <= theta)` 删掉即可。

--------------------------------------------------------------------
真实预期（先说清楚，免得白跑一轮）
--------------------------------------------------------------------
这类策略的合理量级是：年化 10~30%、夏普 0.5~1.0、最大回撤 20~40%，
胜率约 35~45%，收益高度集中在少数几笔大赢家上，中间会有连续
三四个月不赚钱的时期。**它不是"稳定正收益"，是"长期为正但过程难受"。**

因为胜率低、靠少数大赢家，**币对数量和仓位数是关键**：
建议 max_open_trades 提到 8~10，币对扩到 20~30 个。
仓位太少会系统性错过那几笔撑起全部收益的大行情。
====================================================================
"""

from datetime import datetime

import talib.abstract as ta
from pandas import DataFrame, isna

from freqtrade.persistence import Trade
from freqtrade.strategy import DecimalParameter, IntParameter, IStrategy


class Github_AlexEisie_GeniusTraderTaffy__AdaptiveTrend__20260819_144029(IStrategy):
    INTERFACE_VERSION = 3

    # 论文用 6h。config 里不要设 timeframe，否则会覆盖这里
    timeframe = "6h"

    # 现货保持 False。切到永续合约时改成 True，做空逻辑会自动生效
    can_short = False

    # 论文没有止盈，只有移动止损。设成 10（1000%）等于关闭 ROI
    minimal_roi = {"0": 10}

    # 论文没有固定止损。freqtrade 强制要求一个硬上限，
    # 它同时充当 α×ATR 的封顶值（freqtrade 不允许止损比这个更宽）
    stoploss = -0.15
    use_custom_stoploss = True
    trailing_stop = False

    # 出场完全交给移动止损，不使用出场信号（忠实于论文）
    use_exit_signal = False
    exit_profit_only = False
    process_only_new_candles = True

    # 最长回看 60 根 + ATR(14) + 缓冲
    startup_candle_count = 120

    # ---------------- 论文的三个参数 ----------------
    # L：动量回看根数（6h × 28 = 7 天）
    mom_lookback = IntParameter(8, 60, default=28, space="buy", optimize=True)
    # θ_entry：动量阈值
    mom_threshold = DecimalParameter(0.01, 0.12, default=0.04, decimals=3, space="buy", optimize=True)
    # α：ATR 倍数，论文最优 2.5，区间 2.0~3.5
    atr_mult = DecimalParameter(1.5, 4.0, default=2.5, decimals=1, space="sell", optimize=True)

    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        lookback = self.mom_lookback.value

        # MOM_t = (P_t - P_{t-L}) / P_{t-L}
        prev = dataframe["close"].shift(lookback)
        dataframe["mom"] = (dataframe["close"] - prev) / prev

        # ATR，论文用标准周期
        dataframe["atr"] = ta.ATR(dataframe, timeperiod=14)
        dataframe["atr_pct"] = dataframe["atr"] / dataframe["close"]

        return dataframe

    def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        theta = self.mom_threshold.value
        mom = dataframe["mom"]
        alive = (dataframe["volume"] > 0) & (~dataframe["atr_pct"].isna())

        # 做多：动量向上穿越 +θ
        dataframe.loc[
            (mom > theta) & (mom.shift(1) <= theta) & alive,
            ["enter_long", "enter_tag"],
        ] = (1, "mom_up")

        # 做空：动量向下穿越 -θ（can_short=False 时 freqtrade 会自动忽略）
        dataframe.loc[
            (mom < -theta) & (mom.shift(1) >= -theta) & alive,
            ["enter_short", "enter_tag"],
        ] = (1, "mom_down")

        return dataframe

    def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        # 论文没有出场信号，出场只由移动止损触发
        return dataframe

    def custom_stoploss(
        self,
        pair: str,
        trade: Trade,
        current_time: datetime,
        current_rate: float,
        current_profit: float,
        after_fill: bool,
        **kwargs,
    ) -> float | None:
        """
        论文的移动止损：S_t = max(S_{t-1}, P_t - α × ATR_t)

        freqtrade 的 adjust_stop_loss 只会把止损往有利方向移动、
        从不放宽，所以每根 K 线返回「相对当前价 α×ATR」就等价于
        论文里那个 max() 棘轮。返回值的正负号不影响结果。

        止损宽度的上限由 self.stoploss(-0.15) 自动封顶，
        这是 freqtrade 的硬性要求，不是论文的设定。
        """
        dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
        if dataframe is None or len(dataframe) == 0:
            return None

        atr_pct = dataframe["atr_pct"].iloc[-1]
        if isna(atr_pct) or atr_pct <= 0:
            return None

        return -(atr_pct * self.atr_mult.value)

    plot_config = {
        "main_plot": {},
        "subplots": {
            "Momentum": {"mom": {"color": "blue"}},
            "ATR%": {"atr_pct": {"color": "purple"}},
        },
    }
