From cfda8173a09cfaa71d64e91f0f6a15a530f30139 Mon Sep 17 00:00:00 2001 From: "zhangkun9038@dingtalk.com" Date: Sat, 31 May 2025 16:04:05 +0000 Subject: [PATCH] https://x.com/i/grok?conversation=1928233673951957346 --- chat/result13.md | 5 + freqtrade/templates/freqaiprimer.json | 28 ++ freqtrade/templates/freqaiprimer.py | 360 +++++++++++++------------- tools/plot.sh | 11 + 4 files changed, 221 insertions(+), 183 deletions(-) create mode 100644 freqtrade/templates/freqaiprimer.json create mode 100755 tools/plot.sh diff --git a/chat/result13.md b/chat/result13.md index 12b838c9..6190f51e 100644 --- a/chat/result13.md +++ b/chat/result13.md @@ -243,3 +243,8 @@ plot_config = { 如果你希望我帮你生成完整的策略文件,并提供对应的 `config.json` 示例,请告诉我 👇 我可以一步步帮你完成。 +freqtrade plot-dataframe --strategy DivergenceRegressionStrategy \ + --timerange=20250501-20250510 \ + --timeframe=5m \ + --exchange=binance \ + --pair=ETH/USDT diff --git a/freqtrade/templates/freqaiprimer.json b/freqtrade/templates/freqaiprimer.json new file mode 100644 index 00000000..aca584bf --- /dev/null +++ b/freqtrade/templates/freqaiprimer.json @@ -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" +} diff --git a/freqtrade/templates/freqaiprimer.py b/freqtrade/templates/freqaiprimer.py index 9442e69b..6fa8d896 100644 --- a/freqtrade/templates/freqaiprimer.py +++ b/freqtrade/templates/freqaiprimer.py @@ -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 diff --git a/tools/plot.sh b/tools/plot.sh new file mode 100755 index 00000000..7f99c398 --- /dev/null +++ b/tools/plot.sh @@ -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 + +