445 lines
18 KiB
Python
445 lines
18 KiB
Python
"""
|
|
盘整背驰策略 (PanZhengBeiChi Strategy)
|
|
|
|
基于缠论的盘整背驰进行交易:
|
|
- 盘整背驰:同级别走势中,Ai与Ai+2比较力度减弱
|
|
- 顶背驰(卖点):价格创新高或接近,但MACD力度明显减弱
|
|
- 底背驰(买点):价格创新低或接近,但MACD力度明显减弱
|
|
|
|
使用命令:
|
|
freqtrade backtesting -c ./user_data/Chan/config/Local_Test.json \
|
|
--strategy PanZhengBeiChiStrategy --strategy-path ./user_data/Chan/strategies \
|
|
--timerange=20250301-
|
|
"""
|
|
|
|
import logging
|
|
from datetime import datetime
|
|
from typing import Optional
|
|
import numpy as np
|
|
import pandas as pd
|
|
import talib.abstract as ta
|
|
from pandas import DataFrame
|
|
from technical.util import resample_to_interval, resampled_merge
|
|
from freqtrade.strategy import IStrategy
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class PanZhengBeiChiStrategy(IStrategy):
|
|
"""
|
|
盘整背驰策略
|
|
|
|
核心逻辑:
|
|
1. 在5分钟级别识别同级别走势段(Ai)
|
|
2. 比较Ai与Ai+2的MACD力度,判断盘整背驰
|
|
3. 盘整顶背驰(i+2为偶数)-> 卖出
|
|
4. 盘整底背驰(i+2为奇数)-> 买入
|
|
"""
|
|
INTERFACE_VERSION: int = 3
|
|
|
|
# === 基础配置 ===
|
|
timeframe = '1m'
|
|
informative_timeframe = '5m'
|
|
can_short = True
|
|
can_long = True
|
|
|
|
startup_candle_count: int = 2000 # 需要足够的数据来识别走势段
|
|
|
|
# === 止损止盈配置 ===
|
|
stoploss = -0.02 # 2% 硬止损
|
|
use_custom_stoploss = False
|
|
|
|
# Trailing stop
|
|
trailing_stop = True
|
|
trailing_stop_positive = 0.008 # 回撤 0.8% 触发退出
|
|
trailing_stop_positive_offset = 0.015 # 盈利 1.5% 后才开始追踪
|
|
trailing_only_offset_is_reached = True
|
|
|
|
# ROI - 调整止盈策略
|
|
minimal_roi = {
|
|
"0": 0.015, # 1.5% 立即止盈(更保守)
|
|
"30": 0.01, # 30分钟后 1%
|
|
"120": 0.008, # 2小时后 0.8%
|
|
}
|
|
|
|
order_types = {
|
|
"entry": "market",
|
|
"exit": "market",
|
|
"stoploss": "market",
|
|
"stoploss_on_exchange": False,
|
|
}
|
|
|
|
# === 盘整背驰参数 ===
|
|
same_level_timeframe = 5 # 5分钟级别
|
|
pivot_window = 4 # 转折点确认窗口(增大减少噪音)
|
|
min_segment_length = 5 # 最小段长度(K线数)(增大减少假信号)
|
|
|
|
# 背驰判断参数(更严格)
|
|
beichi_price_threshold = 1.10 # 价格涨幅/跌幅阈值(允许10%范围内,更严格)
|
|
beichi_macd_threshold = 0.75 # MACD力度阈值(低于75%即背驰,更严格)
|
|
|
|
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
|
"""计算指标并识别盘整背驰"""
|
|
ticker = self.get_ticker_indicator()
|
|
|
|
# Resample 到 5m 进行同级别分解
|
|
dataframe_5m = resample_to_interval(dataframe, ticker * self.same_level_timeframe)
|
|
|
|
# 在 5m 上计算指标
|
|
dataframe_5m = self.add_indicators_5m(dataframe_5m)
|
|
|
|
# 识别盘整背驰
|
|
dataframe_5m = self.identify_panzheng_beichi(dataframe_5m)
|
|
|
|
# 合并回 1m dataframe
|
|
dataframe = resampled_merge(dataframe, dataframe_5m)
|
|
|
|
# 在 1m 上也计算基础指标
|
|
dataframe = self.add_indicators_1m(dataframe)
|
|
|
|
return dataframe
|
|
|
|
def add_indicators_5m(self, dataframe: DataFrame) -> DataFrame:
|
|
"""在5m级别计算指标"""
|
|
# MACD 用于识别背驰
|
|
macd = ta.MACD(dataframe, fastperiod=12, slowperiod=26, signalperiod=9)
|
|
dataframe['macd'] = macd['macd']
|
|
dataframe['macdsignal'] = macd['macdsignal']
|
|
dataframe['macdhist'] = macd['macdhist']
|
|
|
|
# EMA 用于识别趋势
|
|
dataframe['ema12'] = ta.EMA(dataframe, timeperiod=12)
|
|
dataframe['ema26'] = ta.EMA(dataframe, timeperiod=26)
|
|
dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50)
|
|
dataframe['ema200'] = ta.EMA(dataframe, timeperiod=200)
|
|
|
|
# EMA趋势方向
|
|
dataframe['ema_trend_up'] = (dataframe['ema12'] > dataframe['ema26']) & (dataframe['ema26'] > dataframe['ema50'])
|
|
dataframe['ema_trend_dn'] = (dataframe['ema12'] < dataframe['ema26']) & (dataframe['ema26'] < dataframe['ema50'])
|
|
|
|
# 价格与EMA200关系
|
|
dataframe['price_above_ema200'] = dataframe['close'] > dataframe['ema200']
|
|
dataframe['price_below_ema200'] = dataframe['close'] < dataframe['ema200']
|
|
|
|
# 趋势强度
|
|
dataframe['ema12_slope'] = dataframe['ema12'].diff(5) / dataframe['ema12'].shift(5)
|
|
dataframe['ema26_slope'] = dataframe['ema26'].diff(5) / dataframe['ema26'].shift(5)
|
|
|
|
# 强趋势判断
|
|
dataframe['strong_uptrend'] = (
|
|
(dataframe['ema12_slope'] > 0) &
|
|
(dataframe['ema26_slope'] > 0) &
|
|
(dataframe['price_above_ema200'])
|
|
)
|
|
dataframe['strong_downtrend'] = (
|
|
(dataframe['ema12_slope'] < 0) &
|
|
(dataframe['ema26_slope'] < 0) &
|
|
(dataframe['price_below_ema200'])
|
|
)
|
|
|
|
# RSI
|
|
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
|
|
|
|
# ATR 用于波动率过滤
|
|
dataframe['atr'] = ta.ATR(dataframe, timeperiod=14)
|
|
dataframe['atr_mean'] = dataframe['atr'].rolling(window=20).mean()
|
|
dataframe['volatility_ok'] = dataframe['atr'] > dataframe['atr_mean'] * 0.8
|
|
|
|
return dataframe
|
|
|
|
def add_indicators_1m(self, dataframe: DataFrame) -> DataFrame:
|
|
"""在1m级别计算基础指标"""
|
|
dataframe['rsi_1m'] = ta.RSI(dataframe, timeperiod=14)
|
|
dataframe['volume_mean'] = dataframe['volume'].rolling(window=20).mean()
|
|
|
|
# MACD 用于1m级别确认
|
|
macd_1m = ta.MACD(dataframe, fastperiod=12, slowperiod=26, signalperiod=9)
|
|
dataframe['macd_1m'] = macd_1m['macd']
|
|
dataframe['macdsignal_1m'] = macd_1m['macdsignal']
|
|
dataframe['macdhist_1m'] = macd_1m['macdhist']
|
|
|
|
# MACD交叉
|
|
dataframe['macd_cross_up'] = (
|
|
(dataframe['macd_1m'] > dataframe['macdsignal_1m']) &
|
|
(dataframe['macd_1m'].shift(1) <= dataframe['macdsignal_1m'].shift(1))
|
|
)
|
|
dataframe['macd_cross_dn'] = (
|
|
(dataframe['macd_1m'] < dataframe['macdsignal_1m']) &
|
|
(dataframe['macd_1m'].shift(1) >= dataframe['macdsignal_1m'].shift(1))
|
|
)
|
|
|
|
return dataframe
|
|
|
|
def identify_panzheng_beichi(self, dataframe: DataFrame) -> DataFrame:
|
|
"""
|
|
识别盘整背驰
|
|
|
|
核心逻辑:
|
|
1. 识别局部转折点(高低点)
|
|
2. 构建同级别走势段(Ai)
|
|
3. 比较Ai与Ai+2的MACD力度
|
|
4. 判断盘整背驰:价格涨幅相近但MACD力度减弱
|
|
"""
|
|
df = dataframe.copy()
|
|
window = self.pivot_window
|
|
lookback = window + 1
|
|
|
|
# 初始化列
|
|
df['ai_index'] = -1
|
|
df['ai_type'] = 0 # 1: 上涨, -1: 下跌
|
|
df['ai_high'] = np.nan
|
|
df['ai_low'] = np.nan
|
|
df['ai_macd_max'] = np.nan
|
|
df['ai_macd_min'] = np.nan
|
|
df['beichi_long'] = False # 盘整底背驰(买入信号)
|
|
df['beichi_short'] = False # 盘整顶背驰(卖出信号)
|
|
|
|
# 识别局部高点
|
|
df['temp_high'] = df['high'].shift(window)
|
|
df['is_pivot_high'] = (
|
|
(df['temp_high'] == df['temp_high'].rolling(window=lookback).max()) &
|
|
(df['temp_high'].notna())
|
|
)
|
|
|
|
# 识别局部低点
|
|
df['temp_low'] = df['low'].shift(window)
|
|
df['is_pivot_low'] = (
|
|
(df['temp_low'] == df['temp_low'].rolling(window=lookback).min()) &
|
|
(df['temp_low'].notna())
|
|
)
|
|
|
|
# 逐行处理,识别走势段和背驰
|
|
ai_list = []
|
|
current_ai_start = None
|
|
current_ai_type = None
|
|
last_pivot_idx = None
|
|
|
|
for i in range(window, len(df)):
|
|
# 检查新的转折点
|
|
is_new_pivot = False
|
|
pivot_type = None
|
|
|
|
if df.iloc[i]['is_pivot_high']:
|
|
is_new_pivot = True
|
|
pivot_type = 'high'
|
|
elif df.iloc[i]['is_pivot_low']:
|
|
is_new_pivot = True
|
|
pivot_type = 'low'
|
|
|
|
if is_new_pivot and last_pivot_idx is not None:
|
|
# 完成一个走势段
|
|
if current_ai_start is not None:
|
|
seg_df = df.iloc[current_ai_start:last_pivot_idx]
|
|
if len(seg_df) >= self.min_segment_length:
|
|
high_val = seg_df['high'].max()
|
|
low_val = seg_df['low'].min()
|
|
macd_max = seg_df['macd'].max()
|
|
macd_min = seg_df['macd'].min()
|
|
|
|
# 判断走势类型
|
|
if current_ai_type is None:
|
|
if high_val > df.iloc[current_ai_start]['close']:
|
|
current_ai_type = 1
|
|
else:
|
|
current_ai_type = -1
|
|
|
|
ai_info = {
|
|
'start': current_ai_start,
|
|
'end': last_pivot_idx,
|
|
'type': current_ai_type,
|
|
'high': high_val,
|
|
'low': low_val,
|
|
'macd_max': macd_max,
|
|
'macd_min': macd_min,
|
|
}
|
|
ai_list.append(ai_info)
|
|
|
|
# 标记该段
|
|
df.iloc[current_ai_start:last_pivot_idx, df.columns.get_loc('ai_index')] = len(ai_list) - 1
|
|
df.iloc[current_ai_start:last_pivot_idx, df.columns.get_loc('ai_type')] = current_ai_type
|
|
df.iloc[current_ai_start:last_pivot_idx, df.columns.get_loc('ai_high')] = high_val
|
|
df.iloc[current_ai_start:last_pivot_idx, df.columns.get_loc('ai_low')] = low_val
|
|
df.iloc[current_ai_start:last_pivot_idx, df.columns.get_loc('ai_macd_max')] = macd_max
|
|
df.iloc[current_ai_start:last_pivot_idx, df.columns.get_loc('ai_macd_min')] = macd_min
|
|
|
|
# 判断背驰(Ai与Ai+2比较)
|
|
if len(ai_list) >= 3:
|
|
ai = ai_list[-3] # Ai
|
|
ai_plus_2 = ai_list[-1] # Ai+2
|
|
|
|
if ai['type'] == ai_plus_2['type']:
|
|
# 上涨段:比较向上力度
|
|
if ai['type'] == 1:
|
|
price_chg = (ai_plus_2['high'] - ai_plus_2['low']) / ai_plus_2['low'] if ai_plus_2['low'] > 0 else 0
|
|
price_chg_prev = (ai['high'] - ai['low']) / ai['low'] if ai['low'] > 0 else 0
|
|
macd_chg = ai_plus_2['macd_max']
|
|
macd_chg_prev = ai['macd_max']
|
|
|
|
# 顶背驰:价格涨幅相近但MACD力度减弱
|
|
if price_chg <= price_chg_prev * self.beichi_price_threshold and \
|
|
macd_chg < macd_chg_prev * self.beichi_macd_threshold:
|
|
idx = len(ai_list) - 1 # i+2的索引
|
|
if idx % 2 == 0: # 偶数 -> 卖出
|
|
df.iloc[last_pivot_idx, df.columns.get_loc('beichi_short')] = True
|
|
else: # 奇数 -> 买入
|
|
df.iloc[last_pivot_idx, df.columns.get_loc('beichi_long')] = True
|
|
|
|
# 下跌段:比较向下力度
|
|
else:
|
|
price_chg = abs((ai_plus_2['high'] - ai_plus_2['low']) / ai_plus_2['low']) if ai_plus_2['low'] > 0 else 0
|
|
price_chg_prev = abs((ai['high'] - ai['low']) / ai['low']) if ai['low'] > 0 else 0
|
|
macd_chg = abs(ai_plus_2['macd_min'])
|
|
macd_chg_prev = abs(ai['macd_min'])
|
|
|
|
# 底背驰:价格跌幅相近但MACD力度减弱
|
|
if price_chg <= price_chg_prev * self.beichi_price_threshold and \
|
|
macd_chg < macd_chg_prev * self.beichi_macd_threshold:
|
|
idx = len(ai_list) - 1
|
|
if idx % 2 == 0: # 偶数 -> 卖出
|
|
df.iloc[last_pivot_idx, df.columns.get_loc('beichi_short')] = True
|
|
else: # 奇数 -> 买入
|
|
df.iloc[last_pivot_idx, df.columns.get_loc('beichi_long')] = True
|
|
|
|
# 更新当前段信息
|
|
if pivot_type == 'high':
|
|
current_ai_type = -1 # 高点后向下
|
|
else:
|
|
current_ai_type = 1 # 低点后向上
|
|
current_ai_start = last_pivot_idx
|
|
|
|
if is_new_pivot:
|
|
last_pivot_idx = i
|
|
|
|
return df
|
|
|
|
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
|
ticker = self.get_ticker_indicator()
|
|
resample_col = f"resample_{ticker * self.same_level_timeframe}_"
|
|
|
|
# 5m 级别指标列名
|
|
beichi_long_col = f"{resample_col}beichi_long"
|
|
beichi_short_col = f"{resample_col}beichi_short"
|
|
ai_type_col = f"{resample_col}ai_type"
|
|
rsi_5m_col = f"{resample_col}rsi"
|
|
ema_trend_up_col = f"{resample_col}ema_trend_up"
|
|
ema_trend_dn_col = f"{resample_col}ema_trend_dn"
|
|
volatility_ok_col = f"{resample_col}volatility_ok"
|
|
strong_uptrend_col = f"{resample_col}strong_uptrend"
|
|
strong_downtrend_col = f"{resample_col}strong_downtrend"
|
|
price_above_ema200_col = f"{resample_col}price_above_ema200"
|
|
price_below_ema200_col = f"{resample_col}price_below_ema200"
|
|
|
|
# === 做多入场 ===
|
|
# 条件:盘整底背驰 + 强上升趋势确认
|
|
dataframe.loc[
|
|
(
|
|
# 核心信号:盘整底背驰
|
|
(dataframe[beichi_long_col] == True) &
|
|
|
|
# 强上升趋势确认(更严格)
|
|
(dataframe[strong_uptrend_col] == True) &
|
|
|
|
# RSI 确认(更严格:只在大趋势中操作)
|
|
(dataframe[rsi_5m_col] > 40) &
|
|
(dataframe[rsi_5m_col] < 60) &
|
|
|
|
# 1m 指标确认
|
|
(dataframe['macd_1m'] > dataframe['macdsignal_1m']) &
|
|
|
|
# 成交量确认
|
|
(dataframe['volume'] > dataframe['volume_mean'] * 1.5)
|
|
),
|
|
['enter_long', 'enter_tag']
|
|
] = (1, "pzbc_long")
|
|
|
|
# === 做空入场 ===
|
|
# 条件:盘整顶背驰 + 强下降趋势确认(更严格)
|
|
dataframe.loc[
|
|
(
|
|
# 核心信号:盘整顶背驰
|
|
(dataframe[beichi_short_col] == True) &
|
|
|
|
# 强下降趋势确认
|
|
(dataframe[strong_downtrend_col] == True) &
|
|
|
|
# RSI 确认
|
|
(dataframe[rsi_5m_col] > 40) &
|
|
(dataframe[rsi_5m_col] < 60) &
|
|
|
|
# 1m 指标确认
|
|
(dataframe['macd_1m'] < dataframe['macdsignal_1m']) &
|
|
|
|
# 成交量确认
|
|
(dataframe['volume'] > dataframe['volume_mean'] * 1.5)
|
|
),
|
|
['enter_short', 'enter_tag']
|
|
] = (1, "pzbc_short")
|
|
|
|
return dataframe
|
|
|
|
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
|
"""
|
|
出场逻辑
|
|
|
|
多头出场:
|
|
1. 出现盘整顶背驰
|
|
2. 趋势转弱
|
|
|
|
空头出场:
|
|
1. 出现盘整底背驰
|
|
2. 趋势转弱
|
|
"""
|
|
ticker = self.get_ticker_indicator()
|
|
resample_col = f"resample_{ticker * self.same_level_timeframe}_"
|
|
|
|
beichi_long_col = f"{resample_col}beichi_long"
|
|
beichi_short_col = f"{resample_col}beichi_short"
|
|
ai_type_col = f"{resample_col}ai_type"
|
|
rsi_5m_col = f"{resample_col}rsi"
|
|
ema_trend_dn_col = f"{resample_col}ema_trend_dn"
|
|
strong_downtrend_col = f"{resample_col}strong_downtrend"
|
|
strong_uptrend_col = f"{resample_col}strong_uptrend"
|
|
|
|
# === 多头出场 ===
|
|
dataframe.loc[
|
|
(
|
|
# 出现盘整顶背驰 -> 退出多头
|
|
(dataframe[beichi_short_col] == True) |
|
|
|
|
# 趋势转弱
|
|
(
|
|
(dataframe[ai_type_col] == -1) &
|
|
(dataframe[rsi_5m_col] > 55)
|
|
) |
|
|
|
|
# 强下跌趋势
|
|
(dataframe[strong_downtrend_col] == True)
|
|
),
|
|
['exit_long', 'exit_tag']
|
|
] = (1, "pzbc_exit_long")
|
|
|
|
# === 空头出场 ===
|
|
dataframe.loc[
|
|
(
|
|
# 出现盘整底背驰 -> 退出空头
|
|
(dataframe[beichi_long_col] == True) |
|
|
|
|
# 趋势转弱
|
|
(
|
|
(dataframe[ai_type_col] == 1) &
|
|
(dataframe[rsi_5m_col] < 45)
|
|
) |
|
|
|
|
# 强上涨趋势
|
|
(dataframe[strong_uptrend_col] == True)
|
|
),
|
|
['exit_short', 'exit_tag']
|
|
] = (1, "pzbc_exit_short")
|
|
|
|
return dataframe
|
|
|
|
def get_ticker_indicator(self) -> int:
|
|
"""获取 timeframe 的分钟数"""
|
|
return int(self.timeframe[:-1])
|
|
|