Timeframe
1h
Direction
Long Only
Stoploss
-25.0%
Trailing Stop
No
ROI
0m: 10.0%, 2880m: 2.0%, 5760m: 0.5%
Interface Version
3
Startup Candles
210
Indicators
5
freqtrade/freqtrade-strategies
Strategy 003 author@: Gerald Lonlas github@: https://github.com/freqtrade/freqtrade-strategies
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