zhangkun9038@dingtalk.com 2025-05-31 16:04:05 +00:00
parent 040c5b6c36
commit cfda8173a0
4 changed files with 221 additions and 183 deletions

View File

@ -243,3 +243,8 @@ plot_config = {
如果你希望我帮你生成完整的策略文件,并提供对应的 `config.json` 示例,请告诉我 👇 我可以一步步帮你完成。
freqtrade plot-dataframe --strategy DivergenceRegressionStrategy \
--timerange=20250501-20250510 \
--timeframe=5m \
--exchange=binance \
--pair=ETH/USDT

View File

@ -0,0 +1,28 @@
{
"strategy_name": "FreqaiPrimer",
"params": {
"max_open_trades": {
"max_open_trades": 2
},
"buy": {},
"sell": {},
"protection": {
"cooldown_period": 5
},
"roi": {
"0": 0.02,
"30": 0.01,
"60": 0
},
"stoploss": {
"stoploss": -0.015
},
"trailing": {
"trailing_stop": true,
"trailing_stop_positive": 0.005,
"trailing_stop_positive_offset": 0.01
}
},
"ft_stratparam_v": 1,
"export_time": "2025-05-31 23:03:00.000000+09:00"
}

View File

@ -5,69 +5,66 @@ from functools import reduce
from freqtrade.persistence import Trade
import talib.abstract as ta
from pandas import DataFrame
from typing import Dict, Optional
from technical import qtpylib
from typing import Dict
from freqtrade.strategy import IStrategy
logger = logging.getLogger(__name__)
class FreqaiPrimer(IStrategy):
"""
策略说明
- 所有交易信号基于 FreqAI 的动态预测
- 使用多目标回归模型预测趋势强度波动率ROI止损和背离评分
- 入场/出场基于连续背离信号
- 不使用 Hyperopt 优化参数
- 不包含任何分类列 market_condition
- 只使用回归模型预测价值背离&-price_value_divergence
- 动态调整阈值基于 price_value_divergence 的历史数据
- 放宽过滤条件增加信号触发频率
- 使用RSI和布林带作为辅助过滤条件
"""
# 👇 币种特定参数(可选)
PAIR_PARAMS = {
"BTC/USDT": {"volatility_factor": 1.0, "min_stop": -0.005},
"TON/USDT": {"volatility_factor": 1.3, "min_stop": -0.0035},
"DOGE/USDT": {"volatility_factor": 1.5, "min_stop": -0.003},
"DOT/USDT": {"volatility_factor": 1.3, "min_stop": -0.0035},
"XRP/USDT": {"volatility_factor": 1.35, "min_stop": -0.0032},
"OKB/USDT": {"volatility_factor": 0.9, "min_stop": -0.006},
"SOL/USDT": {"volatility_factor": 1.4, "min_stop": -0.0032},
"WCT/USDT": {"volatility_factor": 1.4, "min_stop": -0.004},
"TRUMP/USDT": {"volatility_factor": 1.4, "min_stop": -0.0035},
"SUI/USDT": {"volatility_factor": 1.6, "min_stop": -0.0025},
"PEPE/USDT": {"volatility_factor": 1.5, "min_stop": -0.0036},
"TRB/USDT": {"volatility_factor": 1.45, "min_stop": -0.004},
"MASK/USDT": {"volatility_factor": 1.65, "min_stop": -0.0045},
"UNI/USDT": {"volatility_factor": 1.45, "min_stop": -0.005},
"KAITO/USDT": {"volatility_factor": 1.45, "min_stop": -0.004},
# 策略参数
minimal_roi = {
"0": 0.02, # 固定止盈2%
"30": 0.01,
"60": 0
}
use_custom_stoploss = True
trailing_stop = False # 关闭 trailing_stop避免干扰背离信号
use_dynamic_roi = True
position_adjuster = True
stoploss = -0.015 # 固定止损-1.5%
timeframe = "3m" # 3分钟K线
use_custom_stoploss = False # 不使用动态止损,改为固定止损
# 绘图配置
plot_config = {
"main_plot": {},
"main_plot": {
"ema200": {"color": "blue"},
"bb_upperband": {"color": "gray"},
"bb_lowerband": {"color": "gray"},
"bb_middleband": {"color": "gray"}
},
"subplots": {
"Signals": {
"enter_long": {"color": "green"},
"exit_long": {"color": "red"}
},
"FreqAI Predictions": {
"&-trend_strength": {"color": "blue"},
"&-volatility_forecast": {"color": "orange"},
"&-divergence_score": {"color": "purple"}
"Price-Value Divergence": {
"&-price_value_divergence": {"color": "purple"}
},
"Volume Z-Score": {
"volume_z_score": {"color": "orange"}
},
"RSI": {
"rsi": {"color": "cyan"}
}
}
}
# FreqAI 配置:回归模型(预测价值背离)
freqai_info = {
"model": "LightGBMMultiTargetRegressor",
"identifier": "divergence_model",
"model": "LightGBMRegressor",
"feature_parameters": {
"include_timeframes": ["5m", "15m", "1h"],
"label_period_candles": 24,
"include_timeframes": ["3m", "15m", "1h"],
"label_period_candles": 12,
"include_shifted_candles": 3,
"principal_component_analysis": True,
},
"data_split_parameters": {
"test_size": 0.2,
@ -79,18 +76,18 @@ class FreqaiPrimer(IStrategy):
"num_leaves": 31,
"verbose": -1,
},
"fit_live_predictions_candles": 100,
}
def __init__(self, config: dict, *args, **kwargs):
super().__init__(config, *args, **kwargs)
# 设置日志级别为 DEBUG
logger.setLevel(logging.DEBUG)
logger.debug("✅ 策略已初始化,日志级别设置为 DEBUG")
# 👇 特征工程函数
def feature_engineering_expand_all(self, dataframe: DataFrame, period: int, metadata: dict, **kwargs) -> DataFrame:
# 保留原有指标
"""
特征工程计算技术指标作为FreqAI的输入特征
"""
dataframe["%-rsi-period"] = ta.RSI(dataframe, timeperiod=period)
dataframe["%-sma-period"] = ta.SMA(dataframe, timeperiod=period)
dataframe["%-ema-period"] = ta.EMA(dataframe, timeperiod=period)
@ -100,192 +97,189 @@ class FreqaiPrimer(IStrategy):
dataframe["bb_lowerband-period"] = lowerband
dataframe["bb_upperband-period"] = upperband
dataframe["bb_middleband-period"] = middleband
dataframe["%-bb_width-period"] = (dataframe["bb_upperband-period"] - dataframe["bb_middleband-period"]) / dataframe["bb_middleband-period"]
dataframe["%-roc-period"] = ta.ROC(dataframe, timeperiod=period)
dataframe["%-bb_width-period"] = (dataframe["bb_upperband-period"] - dataframe["bb_lowerband-period"]) / dataframe["bb_middleband-period"]
# 添加策略 B 的指标
dataframe["%-mfi-period"] = ta.MFI(dataframe, timeperiod=period) # 资金流指标
dataframe["%-adx-period"] = ta.ADX(dataframe, timeperiod=period) # 趋势强度
dataframe["%-tema-period"] = ta.TEMA(dataframe, timeperiod=period) # TEMA 指标
dataframe["%-relative_volume-period"] = dataframe["volume"] / dataframe["volume"].rolling(period).mean() # 相对成交量
dataframe["%-close-bb_lower-period"] = dataframe["close"] / dataframe["bb_lowerband-period"] # 收盘价/布林带下轨
dataframe["%-mfi-period"] = ta.MFI(dataframe, timeperiod=period)
dataframe["%-adx-period"] = ta.ADX(dataframe, timeperiod=period)
dataframe["%-relative_volume-period"] = dataframe["volume"] / dataframe["volume"].rolling(period).mean()
# 新增:与背离相关的衍生特征
dataframe["%-price_trend_diff-period"] = dataframe["close"] - dataframe["%-ema-period"] # 价格与趋势偏离
dataframe["%-roc_mfi_ratio-period"] = dataframe["%-roc-period"] / (dataframe["%-mfi-period"].replace(0, 1)) # ROC/MFI 比率
# 计算价值背离作为特征
dataframe["ema200"] = ta.EMA(dataframe, timeperiod=200)
dataframe["%-price_value_divergence"] = (dataframe["close"] - dataframe["ema200"]) / dataframe["ema200"]
# 数据清理
columns_to_clean = [
"%-rsi-period", "%-mfi-period", "%-sma-period", "%-ema-period", "%-adx-period",
"bb_lowerband-period", "bb_middleband-period", "bb_upperband-period",
"%-bb_width-period", "%-roc-period", "%-relative_volume-period", "%-close-bb_lower-period", "%-tema-period",
"%-price_trend_diff-period", "%-roc_mfi_ratio-period"
"%-bb_width-period", "%-relative_volume-period", "%-price_value_divergence"
]
for col in columns_to_clean:
dataframe[col] = dataframe[col].replace([np.inf, -np.inf], np.nan)
dataframe[col] = dataframe[col].ffill().fillna(0)
dataframe[col] = dataframe[col].replace([np.inf, -np.inf], 0).ffill().fillna(0)
logger.debug(f"[{metadata['pair']}] 特征工程完成,列:{list(dataframe.columns)}")
return dataframe
# 👇 定义目标列(全部为连续值)
def set_freqai_targets(self, dataframe: DataFrame, metadata: dict, **kwargs) -> DataFrame:
label_period = self.freqai_info["feature_parameters"]["label_period_candles"]
"""
设置FreqAI的训练目标只预测价值背离
"""
if len(dataframe) < 200:
logger.warning(f"[{metadata['pair']}] 数据量不足({len(dataframe)}根K线需要至少200根K线进行训练")
return dataframe
# 1. 趋势强度
dataframe["&-trend_strength"] = dataframe["close"].pct_change(label_period)
# 计算200周期EMA作为公平价值
dataframe["ema200"] = ta.EMA(dataframe, timeperiod=200)
# 计算价值背离(回归目标)
dataframe["&-price_value_divergence"] = (dataframe["close"] - dataframe["ema200"]) / dataframe["ema200"]
# 2. 波动率预测
dataframe["&-volatility_forecast"] = dataframe["close"].pct_change(label_period).abs()
# 成交量Z分数
dataframe["volume_mean_20"] = dataframe["volume"].rolling(20).mean()
dataframe["volume_std_20"] = dataframe["volume"].rolling(20).std()
dataframe["volume_z_score"] = (dataframe["volume"] - dataframe["volume_mean_20"]) / dataframe["volume_std_20"]
# 3. ROI 目标
dataframe["&-roi_target"] = np.where(
dataframe["&-trend_strength"] > 0,
dataframe["&-trend_strength"] * 1.5,
0.01
)
# 4. 动态止损目标
dataframe["&-stoploss_target"] = -dataframe["&-volatility_forecast"] * 1.2
# 5. 连续背离评分
dataframe["price_change"] = dataframe["close"].pct_change(label_period)
dataframe["trend_change"] = dataframe["&-trend_strength"].pct_change(label_period)
dataframe["&-divergence_score"] = dataframe["price_change"] - dataframe["trend_change"]
# 👇 新增:清理所有目标列中的非有限值 + clip 背离评分
target_columns = [
"&-trend_strength",
"&-volatility_forecast",
"&-roi_target",
"&-stoploss_target",
"&-divergence_score"
]
for col in target_columns:
if col in dataframe.columns:
dataframe[col] = dataframe[col].replace([np.inf, -np.inf], np.nan)
dataframe[col] = dataframe[col].ffill().fillna(0)
# 对背离评分做裁剪,防止极端值干扰交易信号
dataframe["&-divergence_score"] = dataframe["&-divergence_score"].clip(-1, 1)
# 数据清理
dataframe["&-price_value_divergence"] = dataframe["&-price_value_divergence"].replace([np.inf, -np.inf], 0).ffill().fillna(0)
dataframe["volume_z_score"] = dataframe["volume_z_score"].replace([np.inf, -np.inf], 0).ffill().fillna(0)
logger.debug(f"[{metadata['pair']}] 目标列生成完成,列:{list(dataframe.columns)}")
return dataframe
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe = self.freqai.start(dataframe, metadata, self)
"""
计算所有指标包括FreqAI预测结果并基于历史数据动态调整阈值
"""
logger.info(f"[{metadata['pair']}] 当前可用列调用FreqAI前{list(dataframe.columns)}")
if self.freqai and hasattr(self.freqai, "data"):
labels_mean = self.freqai.data.labels_mean
labels_std = self.freqai.data.labels_std
# 计算200周期EMA和历史价值背离
dataframe["ema200"] = ta.EMA(dataframe, timeperiod=200)
dataframe["price_value_divergence"] = (dataframe["close"] - dataframe["ema200"]) / dataframe["ema200"]
dataframe["mean_trend"] = labels_mean.get("&-trend_strength", 0.0001)
dataframe["std_trend"] = labels_std.get("&-trend_strength", 0.0031)
# 动态计算阈值:基于 price_value_divergence 的历史均值和标准差
if "price_value_divergence" in dataframe.columns:
divergence_mean = dataframe["price_value_divergence"].mean()
divergence_std = dataframe["price_value_divergence"].std()
k = 1.5 # 标准差倍数,可以调整
self.buy_threshold = max(divergence_mean - k * divergence_std, -0.05) # 设置下限,避免过低
self.sell_threshold = min(divergence_mean + k * divergence_std, 0.05) # 设置上限,避免过高
# 确保阈值至少有一定宽度
self.buy_threshold = min(self.buy_threshold, -0.005)
self.sell_threshold = max(self.sell_threshold, 0.005)
logger.info(f"[{metadata['pair']}] price_value_divergence 历史均值:{divergence_mean:.4f}")
logger.info(f"[{metadata['pair']}] price_value_divergence 历史标准差:{divergence_std:.4f}")
logger.info(f"[{metadata['pair']}] 动态买入阈值:{self.buy_threshold:.4f}")
logger.info(f"[{metadata['pair']}] 动态卖出阈值:{self.sell_threshold:.4f}")
else:
self.buy_threshold = -0.015 # 回退阈值
self.sell_threshold = 0.015
logger.warning(f"[{metadata['pair']}] 无法计算动态阈值,使用默认阈值 ±0.015")
dataframe["mean_volatility"] = labels_mean.get("&-volatility_forecast", 0.0021)
dataframe["std_volatility"] = labels_std.get("&-volatility_forecast", 0.0022)
# 调用FreqAI预测价值背离
if not hasattr(self, 'freqai') or self.freqai is None:
logger.error(f"[{metadata['pair']}] FreqAI 未初始化,请确保回测命令中启用了 --freqai")
# 回退到规则计算
dataframe["&-price_value_divergence"] = dataframe["price_value_divergence"]
else:
dataframe = self.freqai.start(dataframe, metadata, self)
dataframe["mean_divergence"] = labels_mean.get("&-divergence_score", 0.0)
dataframe["std_divergence"] = labels_std.get("&-divergence_score", 1.0)
# 检查预测结果
if "&-price_value_divergence" not in dataframe.columns:
logger.warning(f"[{metadata['pair']}] 回归模型未生成 &-price_value_divergence回退到规则计算")
dataframe["&-price_value_divergence"] = dataframe["price_value_divergence"]
# 计算其他指标
upperband, middleband, lowerband = ta.BBANDS(dataframe["close"], timeperiod=20, nbdevup=2.0, nbdevdn=2.0)
dataframe["bb_upperband"] = upperband
dataframe["bb_middleband"] = middleband
dataframe["bb_lowerband"] = lowerband
dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14)
dataframe["volume_mean_20"] = dataframe["volume"].rolling(20).mean()
dataframe["volume_std_20"] = dataframe["volume"].rolling(20).std()
dataframe["volume_z_score"] = (dataframe["volume"] - dataframe["volume_mean_20"]) / dataframe["volume_std_20"]
# 数据清理
for col in ["ema200", "bb_upperband", "bb_middleband", "bb_lowerband", "rsi", "volume_z_score", "&-price_value_divergence", "price_value_divergence"]:
dataframe[col] = dataframe[col].replace([np.inf, -np.inf], 0).ffill().fillna(0)
logger.info(f"[{metadata['pair']}] 指标计算完成,列:{list(dataframe.columns)}")
return dataframe
def populate_entry_trend(self, df: DataFrame, metadata: dict) -> DataFrame:
"""
入场逻辑基于动态阈值和放宽的过滤条件
"""
conditions = []
if "&-divergence_score" in df.columns:
mean_divergence = df["mean_divergence"].iloc[-1] if "mean_divergence" in df.columns else 0
std_divergence = df["std_divergence"].iloc[-1] if "std_divergence" in df.columns else 1
# 计算 Z-score
z_score_divergence = (df["&-divergence_score"] - mean_divergence) / std_divergence
# 正向背离Z-score > 1
buy_condition = z_score_divergence > 1
# 结合趋势强度过滤
if "&-trend_strength" in df.columns:
buy_condition &= df["&-trend_strength"] > 0.005
# 成交量过滤:成交量需大于 20 日均值的 1 倍
df["vol_mean"] = df["volume"].rolling(20).mean()
buy_condition &= (df["volume"] > df["vol_mean"] * 1)
if "&-price_value_divergence" in df.columns:
# 买入条件:价格低估且放量
buy_condition = (df["&-price_value_divergence"] < self.buy_threshold)
buy_condition &= (df["volume_z_score"] > 1.5) # 降低成交量要求
# RSI超卖过滤放宽条件
buy_condition &= (df["rsi"] < 40)
# 价格触及布林带下轨
buy_condition &= (df["close"] <= df["bb_lowerband"])
conditions.append(buy_condition)
else:
logger.warning("⚠️ &-price_value_divergence 列缺失,跳过该条件")
if len(conditions):
if len(conditions) > 0:
df.loc[reduce(lambda x, y: x & y, conditions), 'enter_long'] = 1
logger.info(f"[{metadata['pair']}] 入场信号触发,条件满足")
return df
def populate_exit_trend(self, df: DataFrame, metadata: dict) -> DataFrame:
"""
出场逻辑基于动态阈值和放宽的过滤条件
"""
conditions = []
if "&-divergence_score" in df.columns:
mean_divergence = df["mean_divergence"].iloc[-1] if "mean_divergence" in df.columns else 0
std_divergence = df["std_divergence"].iloc[-1] if "std_divergence" in df.columns else 1
# 计算 Z-score
z_score_divergence = (df["&-divergence_score"] - mean_divergence) / std_divergence
# 负向背离Z-score < -1
sell_condition = z_score_divergence < -1
# 结合趋势强度过滤
if "&-trend_strength" in df.columns:
sell_condition &= df["&-trend_strength"] < -0.005
if "&-price_value_divergence" in df.columns:
# 卖出条件:价格高估
sell_condition = (df["&-price_value_divergence"] > self.sell_threshold)
sell_condition &= (df["volume_z_score"] > 1.5) # 降低成交量要求
# RSI超买过滤放宽条件
sell_condition |= (df["rsi"] > 60)
conditions.append(sell_condition)
else:
logger.warning("⚠️ &-price_value_divergence 列缺失,跳过该条件")
if len(conditions):
if len(conditions) > 0:
df.loc[reduce(lambda x, y: x & y, conditions), 'exit_long'] = 1
logger.info(f"[{metadata['pair']}] 出场信号触发,条件满足")
return df
def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime,
current_rate: float, current_profit: float, **kwargs) -> float:
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
last_candle = dataframe.iloc[-1]
volatility = last_candle.get("&-volatility_forecast", 0.0021)
dynamic_stoploss = -volatility * 2 # 设置为波动率的两倍
dynamic_stoploss = max(dynamic_stoploss, -0.01) # 最大不超过 1%
return dynamic_stoploss
def adjust_trade_position(self, trade: Trade, current_time: datetime,
current_rate: float, current_profit: float,
min_roi: Dict[float, float], max_profit: float) -> Optional[Dict[float, float]]:
dataframe, _ = self.dp.get_analyzed_dataframe(trade.pair, self.timeframe)
last_candle = dataframe.iloc[-1]
roi_target = last_candle.get("&-roi_target", 0.0065)
mean_roi = last_candle.get("mean_roi", 0.0065)
std_roi = last_candle.get("std_roi", 0.0041)
z_score_roi = (roi_target - mean_roi) / std_roi if std_roi != 0 else 0
if z_score_roi > 1:
return {
"0": roi_target * 1.2,
"30": roi_target * 0.9,
"60": roi_target * 0.6,
"120": roi_target * 0.3,
"180": 0.01
}
elif z_score_roi > 0:
return {
"0": roi_target * 1.0,
"20": roi_target * 0.7,
"60": roi_target * 0.3,
"120": 0.01
}
else:
return {
"0": max(0.005, roi_target),
"60": 0.005,
"180": 0.005
}
min_roi: Dict[float, float], max_profit: float):
"""
动态调整仓位持仓时间超过30分钟后强制平仓
"""
hold_time = (current_time - trade.open_date_utc).total_seconds() / 60
if hold_time > 30:
logger.info(f"[{trade.pair}] 持仓时间超过30分钟强制平仓")
return -1
return None
def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float,
time_in_force: str, current_time: datetime, **kwargs) -> bool:
"""
交易确认确保没有快速重复交易
"""
recent_trades = Trade.get_trades(
pair=pair,
is_open=False,
close_date=current_time - datetime.timedelta(minutes=5)
).all()
if len(recent_trades) > 0:
logger.info(f"[{pair}] 5分钟内有近期交易跳过本次入场")
return False
return True
def confirm_trade_exit(self, pair: str, trade: Trade, order_type: str, amount: float,
rate: float, time_in_force: str, exit_reason: str,
current_time: datetime, **kwargs) -> bool:
"""
交易退出确认记录退出原因
"""
logger.info(f"[{pair}] 退出交易,原因:{exit_reason}, 利润:{trade.calc_profit_ratio(rate):.2%}")
return True

11
tools/plot.sh Executable file
View File

@ -0,0 +1,11 @@
#!/bin/bash
docker-compose run --rm freqtrade plot-dataframe \
--logfile /freqtrade/user_data/logs/freqtrade.log \
--freqaimodel LightGBMRegressorMultiTarget \
--config /freqtrade/config_examples/freqaiprimer.json \
--strategy-path /freqtrade/templates \
--strategy FreqaiPrimer \
--timerange 20250410-20250411