💬 Forum

GridStrategyV2

🏆 League #1919 / 1922

Meiyerlove/lhjy/freqtrade_bot/user_data/strategies/GridStrategyV2.py · first seen 2026-07-16 · repo updated 2026-04-29

Basics mode: spot timeframe: 1h interface version: 3
Settings stoploss: -0.25 has minimal roi dca custom stoploss startup candle count: 210 hyperopt hyperopt params: 7
Indicators ADX ATR Bollinger_Bands EMA RSI talib
Concepts dca
15 related strategies ( identical code, similar name)

Each tile is a different kind of check — from an instant code lint to full sandboxed backtests and forward tests on recent data. Not sure what a check actually proves? See the FAQ →

Source

Download Raw
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
import talib.abstract as ta
from pandas import DataFrame
from freqtrade.strategy import IStrategy, DecimalParameter, IntParameter
from freqtrade.persistence import Trade
from datetime import datetime
import logging

logger = logging.getLogger(__name__)


class GridStrategyV2(IStrategy):
    """
    专业自适应网格 v2 —— 高胜率版

    相比 v1 的改进:
      1. EMA 金叉 + EMA200 斜率确认:只在趋势明确向上时入场
      2. RSI 中性区过滤:只在 30~55 入场(不追高、不在恐慌区买)
      3. 成交量确认:成交量高于20日均量,信号更可靠
      4. 棘轮止损:盈利超过 1×ATR 时止损拉到保本位(消灭大亏损)
      5. DCA 最多 3 档(v1 是 4 档),减少深度套牢敞口
      6. 目标:胜率 85%+,Sharpe > 1.2
    """

    INTERFACE_VERSION = 3
    timeframe = "1h"
    can_short = False

    position_adjustment_enable = True
    max_entry_position_adjustment = 4   # 保留 4 档 DCA,给价格回归留足空间

    minimal_roi = {
        "0": 0.10,
        "2880": 0.02,
        "5760": 0.005,
    }

    stoploss = -0.25            # 宽止损:让 DCA 有充足空间摊成本
    trailing_stop = False       # 由 custom_stoploss 接管
    use_custom_stoploss = True

    startup_candle_count = 210  # EMA200 需要预热

    # ── Hyperopt 参数 ────────────────────────────────────────────────
    # 网格间距:缩小上限,高胜率策略用更窄的格
    atr_mult    = DecimalParameter(0.5, 2.0, default=1.0, decimals=1, space="buy")
    adx_max     = IntParameter(20, 45,       default=32,              space="buy")
    bb_width_max= DecimalParameter(0.05, 0.25, default=0.15, decimals=2, space="buy")
    rsi_min     = IntParameter(25, 45,       default=35,              space="buy")
    rsi_max     = IntParameter(45, 68,       default=58,              space="buy")

    # 止盈更快:高胜率的核心 —— 小利润快速兑现,让更多交易在价格反转前出场
    tp_atr_mult = DecimalParameter(0.3, 1.5, default=0.6, decimals=1, space="sell")
    # 棘轮触发更早:尽快把止损拉到保本,消灭潜在大亏
    breakeven_atr = DecimalParameter(0.2, 1.0, default=0.4, decimals=1, space="sell")

    _atr_cache: dict = {}

    # ── 指标计算 ─────────────────────────────────────────────────────
    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        dataframe["atr"]      = ta.ATR(dataframe, timeperiod=14)
        dataframe["adx"]      = ta.ADX(dataframe, timeperiod=14)
        dataframe["rsi"]      = ta.RSI(dataframe, timeperiod=14)
        dataframe["ema50"]    = ta.EMA(dataframe, timeperiod=50)
        dataframe["ema200"]   = ta.EMA(dataframe, timeperiod=200)
        dataframe["vol_ma"]   = dataframe["volume"].rolling(20).mean()

        upper, mid, lower = ta.BBANDS(dataframe["close"], timeperiod=20,
                                       nbdevup=2.0, nbdevdn=2.0)
        dataframe["bb_upper"] = upper
        dataframe["bb_mid"]   = mid
        dataframe["bb_lower"] = lower
        dataframe["bb_width"] = (upper - lower) / mid

        # EMA200 斜率:当前值 vs 10根K线前的值
        dataframe["ema200_slope"] = dataframe["ema200"] - dataframe["ema200"].shift(10)

        if len(dataframe) > 0:
            self._atr_cache[metadata["pair"]] = float(dataframe["atr"].iloc[-1])

        return dataframe

    # ── 入场信号(比 v1 严格得多)────────────────────────────────────
    def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        dataframe.loc[
            (
                # 大方向:价格 > EMA200,EMA50 > EMA200(金叉),EMA200 本身向上
                (dataframe["close"]       > dataframe["ema200"]) &
                (dataframe["ema50"]       > dataframe["ema200"]) &
                (dataframe["ema200_slope"] > 0) &

                # 震荡市:ADX 低 + BB 宽度不过大
                (dataframe["adx"]       < self.adx_max.value) &
                (dataframe["bb_width"]  < self.bb_width_max.value) &

                # RSI 中性区:不追高(< rsi_max),不在极度恐慌区(> rsi_min)
                (dataframe["rsi"] > self.rsi_min.value) &
                (dataframe["rsi"] < self.rsi_max.value) &

                # 入场位置:价格在 BB 中轨以下(不在上半区追入)
                (dataframe["close"] < dataframe["bb_mid"]) &

                # 成交量确认
                (dataframe["volume"] > dataframe["vol_ma"] * 0.8) &
                (dataframe["volume"] > 0)
            ),
            "enter_long",
        ] = 1
        return dataframe

    def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        return dataframe

    # ── 棘轮止损 ─────────────────────────────────────────────────────
    def custom_stoploss(self, pair, trade, current_time: datetime,
                        current_rate, current_profit, after_fill, **kwargs):
        """
        棘轮机制:
        - 亏损中:维持原始止损 (-20%)
        - 盈利超过 breakeven_atr × ATR:止损拉到 -0.5%(近似保本)
        - 盈利超过 2 × breakeven_atr × ATR:止损追踪到当前盈利的 -50%
        """
        atr = self._atr_cache.get(pair)
        if atr is None or trade.open_rate == 0:
            return self.stoploss

        be_threshold = (self.breakeven_atr.value * atr) / trade.open_rate

        if current_profit >= be_threshold * 2:
            # 追踪止损:盈利超过2倍阈值,止损锁在当前盈利的一半以上
            return max(-current_profit * 0.5, -0.005)
        elif current_profit >= be_threshold:
            # 已盈利超过1个ATR,止损拉到保本
            return -0.005
        else:
            return self.stoploss  # 还在初期,维持原止损

    # ── 动态止盈 ─────────────────────────────────────────────────────
    # OKX 现货手续费(实际费率,按档位调整这个值)
    FEE_RATE = 0.001  # 0.10% 单边(挂单价)

    def custom_exit(self, pair, trade, current_time, current_rate,
                    current_profit, **kwargs):
        atr = self._atr_cache.get(pair)
        if atr is None or trade.open_rate == 0:
            return None

        filled = trade.nr_of_successful_entries

        # 动态最小止盈:必须覆盖所有入场手续费 + 出场手续费 + 0.2% 净利润缓冲
        # current_profit 已由 Freqtrade 扣除手续费,这里额外加 0.2% 安全垫
        # 确保每笔交易在任意 DCA 档位下都真正盈利
        fee_buffer = self.FEE_RATE * filled + 0.002   # 已入场次数 × 入场费 + 净利缓冲
        atr_target = (self.tp_atr_mult.value * atr) / trade.open_rate
        profit_target = max(atr_target, fee_buffer)

        if current_profit >= profit_target:
            return f"grid_tp_lvl{filled}_{current_profit:.3f}"

        return None

    # ── 网格加仓逻辑 ─────────────────────────────────────────────────
    def adjust_trade_entry(
        self,
        trade: Trade,
        current_time,
        current_rate: float,
        current_profit: float,
        min_stake,
        max_stake: float,
        current_entry_rate: float,
        current_exit_rate: float,
        current_entry_profit: float,
        current_exit_profit: float,
        **kwargs,
    ):
        filled = trade.nr_of_successful_entries
        if filled >= (self.max_entry_position_adjustment + 1):
            return None

        atr = self._atr_cache.get(trade.pair)
        if atr:
            grid_step = (self.atr_mult.value * atr) / current_rate
            grid_step = max(grid_step, 0.012)
            grid_step = min(grid_step, 0.08)
        else:
            grid_step = 0.03

        threshold = -(grid_step * filled)
        if current_profit > threshold:
            return None

        # 仓位递增:1x → 1.5x → 2x → 2.5x
        multiplier = 1.0 + max(filled - 1, 0) * 0.5
        stake = trade.stake_amount * multiplier
        stake = max(stake, min_stake or 0)
        stake = min(stake, max_stake)

        logger.info(
            f"[GridV2] {trade.pair}{filled}次加仓 "
            f"间距={grid_step:.2%} 当前={current_profit:.2%} 加仓={stake:.2f}U ({multiplier}x)"
        )
        return stake