Files
Chan/strategies/EMA_Pattern.py
2026-02-04 22:53:52 +08:00

437 lines
19 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# --- Do not remove these libs ---
from freqtrade.strategy import IStrategy
from pandas import DataFrame
import talib.abstract as ta
import numpy as np
# 1小时短线趋势追踪策略 V7 - EMA + MACD 综合趋势判断
# 使用 EMA8, EMA24, EMA72, EMA168 + MACD 识别趋势阶段和强度
# freqtrade backtesting -c ./user_data/Chan/config/EMA_Pattern.json --strategy EMA_Pattern --strategy-path ./user_data/Chan/strategies --timerange=20250101-
# freqtrade download-data -c ./user_data/Chan/config/EMA_Pattern.json -t 1h --pairs BTC/USDT:USDT --timerange=20250101-
class EMA_Pattern(IStrategy):
"""
1小时K线短线趋势追踪策略 V7 - EMA + MACD
核心逻辑:
1. EMA多头排列 + MACD金叉确认 = 趋势启动(最佳入场)
2. EMA发散 + MACD动能增加 = 趋势发展(可追涨)
3. MACD动能减弱/顶背离 = 趋势成熟(减仓)
4. EMA死叉 + MACD死叉 = 趋势结束(出场)
"""
can_short: bool = True
timeframe: str = "1h"
process_only_new_candles: bool = True
# EMA参数
ema_fast: int = 8
ema_short: int = 24
ema_mid: int = 72
ema_long: int = 168
# 趋势强度阈值(放宽以增加交易机会)
min_trend_strength: int = 65
# ADX阈值
min_adx: int = 25 # 有趋势就交易
# ROI - V6最佳参数(无杠杆)
minimal_roi = {
"120": 0.02, # 5天后 2%止盈
"72": 0.03, # 3天后 3%止盈
"24": 0.05, # 1天后 5%止盈
"0": 0.08, # 立即 8%止盈
}
# 固定止损
stoploss: float = -0.05 # 5%止损
# 追踪止损
trailing_stop: bool = True
trailing_stop_positive: float = 0.025 # 盈利2.5%后启动
trailing_stop_positive_offset: float = 0.04 # 盈利4%后才触发
trailing_only_offset_is_reached: bool = True
# 禁用自定义止损(让追踪止损工作)
use_custom_stoploss: bool = False
# 只做多(做空效果差)
can_short: bool = False
startup_candle_count: int = 250
def custom_stoploss(self, pair: str, trade, current_time, current_rate,
current_profit, after_fill, **kwargs) -> float:
"""
动态止损 V3 - 保护利润但不过早出场:
1. 盈利超过6%,止损移到盈利3%(锁定一半利润)
2. 盈利超过3%,止损移到盈利1%
3. 盈利超过1.5%,止损移到保本
4. 长时间亏损才考虑缩紧止损
"""
# 持仓时间(小时)
trade_duration = (current_time - trade.open_date_utc).total_seconds() / 3600
# 盈利时动态止损 - 阶梯式保护利润
if current_profit > 0.06:
# 盈利超过6%,锁定3%利润
return -0.03
elif current_profit > 0.03:
# 盈利超过3%,锁定1%利润
return -0.02
elif current_profit > 0.015:
# 盈利超过1.5%,移到保本
return -0.005
# 长时间持仓亏损(超过96小时=4天),才缩紧止损
if trade_duration > 96 and current_profit < -0.03:
return -0.04 # 缩紧到4%
# 默认使用配置的止损
return self.stoploss
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
if dataframe is None or dataframe.empty:
return dataframe
# ==================== EMA指标 ====================
dataframe['ema8'] = ta.EMA(dataframe, timeperiod=self.ema_fast)
dataframe['ema24'] = ta.EMA(dataframe, timeperiod=self.ema_short)
dataframe['ema72'] = ta.EMA(dataframe, timeperiod=self.ema_mid)
dataframe['ema168'] = ta.EMA(dataframe, timeperiod=self.ema_long)
# ==================== MACD指标 ====================
macd = ta.MACD(dataframe, fastperiod=12, slowperiod=26, signalperiod=9)
dataframe['macd'] = macd['macd']
dataframe['macd_signal'] = macd['macdsignal']
dataframe['macd_hist'] = macd['macdhist']
# MACD辅助指标
dataframe['macd_hist_change'] = dataframe['macd_hist'] - dataframe['macd_hist'].shift(1)
dataframe['macd_hist_ma'] = dataframe['macd_hist'].rolling(5).mean()
# ADX趋势强度
dataframe['adx'] = ta.ADX(dataframe, timeperiod=14)
# ATR波动率
dataframe['atr'] = ta.ATR(dataframe, timeperiod=14)
dataframe['atr_pct'] = dataframe['atr'] / dataframe['close'] * 100
# 成交量
dataframe['volume_ma'] = dataframe['volume'].rolling(20).mean()
dataframe['volume_ratio'] = dataframe['volume'] / dataframe['volume_ma']
# ==================== 均线排列 ====================
dataframe['bull_align'] = (
(dataframe['ema8'] > dataframe['ema24']) &
(dataframe['ema24'] > dataframe['ema72']) &
(dataframe['ema72'] > dataframe['ema168'])
)
dataframe['bear_align'] = (
(dataframe['ema8'] < dataframe['ema24']) &
(dataframe['ema24'] < dataframe['ema72']) &
(dataframe['ema72'] < dataframe['ema168'])
)
# 简化排列(短中期)
dataframe['bull_align_short'] = (
(dataframe['ema8'] > dataframe['ema24']) &
(dataframe['ema24'] > dataframe['ema72'])
)
dataframe['bear_align_short'] = (
(dataframe['ema8'] < dataframe['ema24']) &
(dataframe['ema24'] < dataframe['ema72'])
)
# ==================== 均线斜率 ====================
lookback = 5
dataframe['slope_ema8'] = (dataframe['ema8'] - dataframe['ema8'].shift(lookback)) / dataframe['ema8'].shift(lookback) * 100
dataframe['slope_ema24'] = (dataframe['ema24'] - dataframe['ema24'].shift(lookback)) / dataframe['ema24'].shift(lookback) * 100
dataframe['slope_ema72'] = (dataframe['ema72'] - dataframe['ema72'].shift(lookback)) / dataframe['ema72'].shift(lookback) * 100
dataframe['slope_ema168'] = (dataframe['ema168'] - dataframe['ema168'].shift(lookback)) / dataframe['ema168'].shift(lookback) * 100
dataframe['slope_bull_confirm'] = (
(dataframe['slope_ema8'] > 0) &
(dataframe['slope_ema24'] > 0) &
(dataframe['slope_ema72'] > 0)
)
dataframe['slope_bear_confirm'] = (
(dataframe['slope_ema8'] < 0) &
(dataframe['slope_ema24'] < 0) &
(dataframe['slope_ema72'] < 0)
)
# ==================== 均线间距 ====================
dataframe['spread_total'] = (dataframe['ema8'] - dataframe['ema168']) / dataframe['ema168'] * 100
dataframe['spread_short'] = (dataframe['ema8'] - dataframe['ema24']) / dataframe['ema24'] * 100
dataframe['spread_change'] = dataframe['spread_total'] - dataframe['spread_total'].shift(lookback)
dataframe['spread_ma'] = dataframe['spread_total'].rolling(20).mean()
spread_abs = dataframe['spread_total'].abs()
dataframe['spread_pct_30'] = spread_abs.rolling(50, min_periods=20).quantile(0.3)
dataframe['spread_pct_70'] = spread_abs.rolling(50, min_periods=20).quantile(0.7)
# ==================== MACD交叉信号 ====================
# MACD金叉(MACD线上穿信号线)
dataframe['macd_golden_cross'] = (
(dataframe['macd'] > dataframe['macd_signal']) &
(dataframe['macd'].shift(1) <= dataframe['macd_signal'].shift(1))
)
# MACD死叉
dataframe['macd_death_cross'] = (
(dataframe['macd'] < dataframe['macd_signal']) &
(dataframe['macd'].shift(1) >= dataframe['macd_signal'].shift(1))
)
# MACD零轴上方金叉(更强信号)
dataframe['macd_strong_golden'] = dataframe['macd_golden_cross'] & (dataframe['macd'] > 0)
# ==================== MACD动能判断 ====================
# 柱状图动能增加(多头)
dataframe['macd_momentum_up'] = (
(dataframe['macd_hist'] > 0) &
(dataframe['macd_hist'] > dataframe['macd_hist'].shift(1)) &
(dataframe['macd_hist'].shift(1) > dataframe['macd_hist'].shift(2))
)
# 柱状图动能增加(空头)
dataframe['macd_momentum_down'] = (
(dataframe['macd_hist'] < 0) &
(dataframe['macd_hist'] < dataframe['macd_hist'].shift(1)) &
(dataframe['macd_hist'].shift(1) < dataframe['macd_hist'].shift(2))
)
# 柱状图动能减弱(顶部信号)
dataframe['macd_momentum_weakening'] = (
(dataframe['macd_hist'] > 0) &
(dataframe['macd_hist'] < dataframe['macd_hist'].shift(1)) &
(dataframe['macd_hist'].shift(1) < dataframe['macd_hist'].shift(2))
)
# ==================== 趋势阶段(EMA + MACD综合判断)====================
# 趋势启动期:EMA开始排列 + MACD金叉 + 柱状图正向增长
dataframe['trend_start_bull'] = (
dataframe['bull_align_short'] & # 至少短中期排列
(dataframe['macd'] > dataframe['macd_signal']) & # MACD金叉状态
(dataframe['macd_hist'] > 0) & # 柱状图为正
(dataframe['macd_hist'] > dataframe['macd_hist'].shift(1)) & # 动能增加
(spread_abs < dataframe['spread_pct_70']) # 发散度不是最大
)
# 趋势发展期:完整排列 + MACD在零轴上方 + 持续放量
dataframe['trend_develop_bull'] = (
dataframe['bull_align'] & # 完整4线排列
(dataframe['macd'] > 0) & # MACD在零轴上方
(dataframe['macd'] > dataframe['macd_signal']) & # 金叉状态
dataframe['slope_bull_confirm'] & # 斜率确认
(dataframe['spread_change'] > 0) # 发散度扩大
)
# 趋势成熟期:发散度大 + MACD动能减弱
dataframe['trend_mature_bull'] = (
dataframe['bull_align'] &
(dataframe['macd'] > 0) &
dataframe['macd_momentum_weakening'] & # 动能减弱
(spread_abs > dataframe['spread_pct_70']) # 发散度很大
)
# 趋势结束:EMA开始死叉 + MACD死叉
dataframe['trend_end_bull'] = (
(dataframe['ema8'] < dataframe['ema24']) & # EMA8死叉EMA24
(dataframe['macd'] < dataframe['macd_signal']) & # MACD死叉
(dataframe['macd_hist'] < 0) # 柱状图转负
)
# 空头阶段(镜像)
dataframe['trend_start_bear'] = (
dataframe['bear_align_short'] &
(dataframe['macd'] < dataframe['macd_signal']) &
(dataframe['macd_hist'] < 0) &
(dataframe['macd_hist'] < dataframe['macd_hist'].shift(1)) &
(spread_abs < dataframe['spread_pct_70'])
)
dataframe['trend_develop_bear'] = (
dataframe['bear_align'] &
(dataframe['macd'] < 0) &
(dataframe['macd'] < dataframe['macd_signal']) &
dataframe['slope_bear_confirm'] &
(dataframe['spread_change'] < 0)
)
dataframe['trend_end_bear'] = (
(dataframe['ema8'] > dataframe['ema24']) &
(dataframe['macd'] > dataframe['macd_signal']) &
(dataframe['macd_hist'] > 0)
)
# ==================== 趋势强度计算 (0-100) ====================
# 1. 排列得分 (0-25)
bull_align_score = (
(dataframe['ema8'] > dataframe['ema24']).astype(int) +
(dataframe['ema24'] > dataframe['ema72']).astype(int) +
(dataframe['ema72'] > dataframe['ema168']).astype(int) +
(dataframe['close'] > dataframe['ema8']).astype(int)
) * 6.25
bear_align_score = (
(dataframe['ema8'] < dataframe['ema24']).astype(int) +
(dataframe['ema24'] < dataframe['ema72']).astype(int) +
(dataframe['ema72'] < dataframe['ema168']).astype(int) +
(dataframe['close'] < dataframe['ema8']).astype(int)
) * 6.25
dataframe['align_score'] = np.maximum(bull_align_score, bear_align_score)
# 2. 斜率得分 (0-25)
dataframe['slope_score'] = np.clip(np.abs(dataframe['slope_ema24']) * 10, 0, 25)
# 3. MACD动能得分 (0-25) - 替换原来的间距得分
macd_hist_norm = dataframe['macd_hist'].abs() / dataframe['close'] * 1000
dataframe['macd_score'] = np.clip(macd_hist_norm * 5, 0, 25)
# 4. ADX得分 (0-25)
dataframe['adx_score'] = np.clip(dataframe['adx'] - 15, 0, 25)
# 综合强度
dataframe['trend_strength'] = (
dataframe['align_score'] +
dataframe['slope_score'] +
dataframe['macd_score'].fillna(12.5) +
dataframe['adx_score']
)
# ==================== 入场信号 ====================
# EMA金叉
dataframe['ema8_cross_ema24_up'] = (
(dataframe['ema8'] > dataframe['ema24']) &
(dataframe['ema8'].shift(1) <= dataframe['ema24'].shift(1))
)
dataframe['ema8_cross_ema24_down'] = (
(dataframe['ema8'] < dataframe['ema24']) &
(dataframe['ema8'].shift(1) >= dataframe['ema24'].shift(1))
)
# 回踩EMA24反弹
dataframe['pullback_buy'] = (
(dataframe['low'].shift(1) <= dataframe['ema24'].shift(1) * 1.005) &
(dataframe['close'] > dataframe['ema8']) &
(dataframe['close'] > dataframe['open']) &
(dataframe['macd'] > dataframe['macd_signal']) # MACD确认
)
# 突破前高
dataframe['high_break'] = dataframe['close'] > dataframe['high'].rolling(24).max().shift(1)
dataframe['low_break'] = dataframe['close'] < dataframe['low'].rolling(24).min().shift(1)
# ==================== 大周期趋势过滤 ====================
dataframe['big_trend_bull'] = (
(dataframe['ema168'] > dataframe['ema168'].shift(24)) &
(dataframe['close'] > dataframe['ema168'])
)
dataframe['big_trend_bear'] = (
(dataframe['ema168'] < dataframe['ema168'].shift(24)) &
(dataframe['close'] < dataframe['ema168'])
)
return dataframe
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
if dataframe is None or dataframe.empty:
return dataframe
# ==================== 做多入场1:趋势启动(EMA排列 + MACD确认)====================
# 核心条件:EMA开始多头排列 + MACD金叉 + 动能增加
dataframe.loc[
(
dataframe['bull_align_short'] & # 至少短中期排列(EMA8>24>72
(dataframe['macd'] > dataframe['macd_signal']) & # MACD金叉
(dataframe['macd_hist'] > 0) & # 柱状图为正
(dataframe['macd_hist'] > dataframe['macd_hist'].shift(1)) & # 动能增加
(dataframe['close'] > dataframe['ema24']) & # 价格在EMA24上方
(dataframe['adx'] > 20) & # 有趋势
(dataframe['slope_ema8'] > 0) & # EMA8向上
(dataframe['volume_ratio'] > 0.7)
),
["enter_long", "enter_tag"],
] = (1, "trend_start")
# ==================== 做多入场2:强势突破(突破前高 + MACD确认)====================
dataframe.loc[
(
dataframe['bull_align'] & # 完整排列
dataframe['high_break'] & # 突破前高
(dataframe['macd'] > dataframe['macd_signal']) & # MACD金叉状态
(dataframe['adx'] > self.min_adx) & # 强趋势
(dataframe['trend_strength'] >= self.min_trend_strength - 10) &
(dataframe['volume_ratio'] > 0.9)
),
["enter_long", "enter_tag"],
] = (1, "bull_breakout")
# ==================== 做多入场3:回踩反弹(趋势中回调)====================
dataframe.loc[
(
dataframe['bull_align'] & # 完整排列
(dataframe['low'].shift(1) <= dataframe['ema24'].shift(1) * 1.01) & # 回踩EMA24附近
(dataframe['close'] > dataframe['open']) & # 阳线
(dataframe['close'] > dataframe['ema8']) & # 收盘价回到EMA8上方
(dataframe['macd'] > 0) & # MACD在零轴上方
(dataframe['macd'] > dataframe['macd_signal']) & # 金叉状态
(dataframe['trend_strength'] >= self.min_trend_strength - 15)
),
["enter_long", "enter_tag"],
] = (1, "bull_pullback")
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
if dataframe is None or dataframe.empty:
return dataframe
# ==================== 多头出场1:趋势结束(EMA死叉 + MACD死叉)====================
dataframe.loc[
dataframe['trend_end_bull'] & # EMA8死叉 + MACD死叉 + 柱状图转负
(dataframe['close'] < dataframe['ema24']), # 价格确认跌破EMA24
["exit_long", "exit_tag"],
] = (1, "trend_end")
# ==================== 多头出场2:趋势反转(完全转为空头)====================
dataframe.loc[
dataframe['bear_align'] & # 完全空头排列
(dataframe['macd'] < dataframe['macd_signal']) & # MACD死叉
(dataframe['macd_hist'] < 0) & # 柱状图为负
(dataframe['close'] < dataframe['ema72']) & # 价格跌破EMA72
(dataframe['close'].shift(1) < dataframe['ema72'].shift(1)), # 连续2根确认
["exit_long", "exit_tag"],
] = (1, "trend_reverse")
# ==================== 空头出场1:趋势结束 ====================
dataframe.loc[
dataframe['trend_end_bear'] &
(dataframe['close'] > dataframe['ema24']),
["exit_short", "exit_tag"],
] = (1, "trend_end")
# ==================== 空头出场2:趋势反转 ====================
dataframe.loc[
dataframe['bull_align'] &
(dataframe['macd'] > dataframe['macd_signal']) &
(dataframe['macd_hist'] > 0) &
(dataframe['close'] > dataframe['ema72']) &
(dataframe['close'].shift(1) > dataframe['ema72'].shift(1)),
["exit_short", "exit_tag"],
] = (1, "trend_reverse")
return dataframe
def get_ticker_indicator(self):
return int(self.timeframe[:-1])