💬 Forum

TrendFollowingStrategy

🏆 League #1531 / 1937

bench-prog/AiQuant/freqtrade/user_data/strategies/strategy_trend_following_v1.py · first seen 2026-07-16 · repo updated 2026-05-31 · ⬇ 1 download

Basics mode: spot timeframe: 4h
Settings stoploss: -0.05 has minimal roi trailing custom stoploss process only new candles hyperopt hyperopt params: 4
Concepts trailing
Methods custom_stoploss
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 →

  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
"""
趋势跟踪策略 (TrendFollowingStrategy)

纯规则驱动,零 ML 依赖:
  - 入场: EMA(20) > EMA(50) > EMA(200) 多头排列
  - 过滤: ADX > 20(只做趋势市)
  - 退出: EMA(20) < EMA(50) 或止损
  - 止损: 2x ATR 跟踪止损
"""

import logging
from typing import Optional

import pandas as pd
import numpy as np
from freqtrade.strategy import IStrategy, IntParameter, DecimalParameter

logger = logging.getLogger(__name__)


class TrendFollowingStrategy(IStrategy):
    timeframe = "4h"
    can_short = False
    process_only_new_candles = True

    # Risk
    stoploss = -0.05
    trailing_stop = True
    trailing_stop_positive = 0.02
    trailing_stop_positive_offset = 0.03
    trailing_only_offset_is_reached = True

    # ROI
    minimal_roi = {"0": 0.10, "24": 0.05, "48": 0.02}

    # Hyperopt
    adx_threshold = IntParameter(15, 30, default=20, space="buy")
    ema_short = IntParameter(5, 20, default=10, space="buy")
    ema_mid = IntParameter(20, 50, default=30, space="buy")
    ema_long = IntParameter(80, 150, default=100, space="buy")

    def informative_pairs(self):
        return []

    def populate_indicators(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame:
        # EMAs
        dataframe["ema_short"] = dataframe["close"].ewm(span=self.ema_short.value).mean()
        dataframe["ema_mid"] = dataframe["close"].ewm(span=self.ema_mid.value).mean()
        dataframe["ema_long"] = dataframe["close"].ewm(span=self.ema_long.value).mean()

        # ADX
        high, low, close = dataframe["high"], dataframe["low"], dataframe["close"]
        tr = pd.DataFrame({
            "hl": high - low,
            "hc": (high - close.shift()).abs(),
            "lc": (low - close.shift()).abs(),
        }).max(axis=1)
        atr = tr.ewm(span=14).mean()

        up = high.diff()
        down = -low.diff()
        plus_dm = up.where((up > 0) & (up > down), 0.0)
        minus_dm = down.where((down > 0) & (down > up), 0.0)
        plus_di = 100 * (plus_dm.ewm(span=14).mean() / atr)
        minus_di = 100 * (minus_dm.ewm(span=14).mean() / atr)
        dx = 100 * (plus_di - minus_di).abs() / (plus_di + minus_di)
        dataframe["adx"] = dx.ewm(span=14).mean()

        # ATR for stop
        dataframe["atr"] = atr

        return dataframe

    def populate_entry_trend(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame:
        dataframe["enter_long"] = 0

        # 多头排列: ema_short > ema_mid > ema_long
        trend_up = (
            (dataframe["ema_short"] > dataframe["ema_mid"])
            & (dataframe["ema_mid"] > dataframe["ema_long"])
        )
        # ADX 过滤
        trending = dataframe["adx"] > self.adx_threshold.value
        # 价格在短均线上方
        price_ok = dataframe["close"] > dataframe["ema_short"]

        dataframe.loc[trend_up & trending & price_ok, "enter_long"] = 1
        return dataframe

    def populate_exit_trend(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame:
        dataframe["exit_long"] = 0

        # 趋势反转: ema_short < ema_mid
        trend_down = dataframe["ema_short"] < dataframe["ema_mid"]
        dataframe.loc[trend_down, "exit_long"] = 1

        return dataframe

    def custom_stoploss(
        self, pair: str, trade, current_time, current_rate, current_profit,
        after_fill: bool, **kwargs
    ) -> Optional[float]:
        # 使用 ATR 动态止损
        if after_fill:
            return None

        dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
        if dataframe is None or len(dataframe) == 0:
            return None

        last = dataframe.iloc[-1]
        atr = last.get("atr", 0)
        close = last.get("close", current_rate)

        if atr > 0 and close > 0:
            atr_pct = atr / close
            return -max(2.0 * atr_pct, 0.03)  # 至少 3% 止损

        return None