diff --git a/freqtrade/templates/freqaiprimer.py b/freqtrade/templates/freqaiprimer.py index 12fe4695..ce18b98e 100644 --- a/freqtrade/templates/freqaiprimer.py +++ b/freqtrade/templates/freqaiprimer.py @@ -6,8 +6,6 @@ import os import json import glob import redis -import socket -import time from functools import reduce from freqtrade.persistence import Trade import talib.abstract as ta @@ -15,7 +13,6 @@ from pandas import DataFrame import pandas as pd from typing import Dict from freqtrade.strategy import (DecimalParameter, IStrategy, IntParameter) -from freqtrade.enums import RunMode from datetime import datetime logger = logging.getLogger(__name__) @@ -29,7 +26,7 @@ class FreqaiPrimer(IStrategy): # --- 🧪 固定配置参数(从hyperopt优化结果获取)--- TRAILING_STOP_START = 0.045 - TRAILING_STOP_DISTANCE = 0.015 + TRAILING_STOP_DISTANCE = 0.0125 BUY_THRESHOLD_MIN = -0.035 BUY_THRESHOLD_MAX = -0.001 @@ -53,9 +50,6 @@ class FreqaiPrimer(IStrategy): TREND_FINAL_BULLISH_THRESHOLD = 55 # 上涨趋势最终阈值 TREND_FINAL_BEARISH_THRESHOLD = 13 # 下跌趋势最终阈值 - # 🎯 绿色通道折扣常量(不走Hyperopt,单独维护) - GREEN_CHANNEL_DISCOUNT = 0.025 # 2.5% 固定折扣 - # Hyperopt 可优化参数 - 基于初步结果调整范围 trend_final_bullish_threshold = IntParameter(20, 85, default=71, space="buy", optimize=True, load=True) # 降低上限,避免过于保守 trend_final_bearish_threshold = IntParameter(5, 45, default=21, space="buy", optimize=True, load=True) # 扩大下限,捕获更多熊市机会 @@ -93,32 +87,22 @@ class FreqaiPrimer(IStrategy): exit_ranging_trend_score_max = IntParameter(95, 99, default=98, space="sell", optimize=True, load=True) exit_ranging_trend_score_threshold = IntParameter(80, 90, default=85, space="sell", optimize=True, load=True) - - # 🎯 入场折扣率参数(可通过hyperopt优化) - entry_discount_bull_normal = DecimalParameter(0.005, 0.03, default=0.010, decimals=3, space="buy", optimize=True, load=True) # 牛市正常通道折扣 (默认1%) - entry_discount_ranging = DecimalParameter(0.001, 0.02, default=0.0075, decimals=3, space="buy", optimize=True, load=True) # 震荡市折扣 (默认0.75%) - entry_discount_bearish = DecimalParameter(0.001, 0.015, default=0.005, decimals=3, space="buy", optimize=True, load=True) # 熊市折扣 (默认0.5%) - - # 🎯 加仓策略参数(统一标准,移除趋势判断) - ADD_PROGRESSION_FACTOR = 1.09 # 递进系数:每次加仓阈值递增1.09倍 - # --- 🛠️ 固定配置参数(优化提高换手率)--- - stoploss = -0.13 # 🎯 平衡止损:既控制风险又给趋势空间 + # --- 🛠️ 固定配置参数 --- + stoploss = -0.15 timeframe = "3m" use_custom_stoploss = True position_adjustment_enable = True # 启用动态仓位调整 trailing_stop = True - trailing_stop_positive = 0.008 # 🎯 降低锁定利润到0.8% - trailing_stop_positive_offset = 0.015 # 🎯 降低启动阈值到1.5% + trailing_stop_positive = 0.0125 # 🎯 降低锁定利润到2.5% + trailing_stop_positive_offset = 0.04 # 🎯 降低启动阈值到8% trailing_only_offset_is_reached = False - # 🎯 激进ROI设置,强制短时间出场提高换手率 minimal_roi = { - "0": 0.025, # 立即出场,2.5%盈利即走 - "15": 0.015, # 45分钟内,1.5%盈利出场 - "30": 0.008, # 90分钟内,0.8%盈利出场 - "60": 0.003, # 180分钟内,0.3%盈利出场 - "90": 0.0 # 270分钟内,强制出场 + "0": 0.02, # 30分钟(0-30分钟)内,8% 盈利退出 + "30": 0.04, # 2小时(30-120分钟)内,4% 盈利退出 + "90": 0.025, # 4小时(120-240分钟)内,2% 盈利退出 + "270": -0.005 # 8小时(240-480分钟)内,0% 盈利退出 } plot_config = { @@ -156,7 +140,6 @@ class FreqaiPrimer(IStrategy): }, "data_split_parameters": { "test_size": 0.2, - "random_state": 42, "shuffle": False, }, "model_training_parameters": { @@ -212,6 +195,42 @@ class FreqaiPrimer(IStrategy): "live_retrain_candles": 100, } + @property + def protections(self): + """ + 保护机制配置 + 基于最新Freqtrade规范,保护机制应定义在策略文件中而非配置文件 + """ + return [ + { + "method": "StoplossGuard", + "lookback_period_candles": 60, # 3小时回看期(60根3分钟K线) + "trade_limit": 2, # 最多2笔止损交易 + "stop_duration_candles": 60, # 暂停180分钟(60根3分钟K线) + "only_per_pair": True # 仅针对单个币对 + }, + { + "method": "CooldownPeriod", + "stop_duration_candles": 2 # 6分钟冷却期(2根3分钟K线) + }, + { + "method": "MaxDrawdown", + "lookback_period_candles": 48, # 2.4小时回看期 + "trade_limit": 4, # 4笔交易限制 + "stop_duration_candles": 24, # 72分钟暂停(24根3分钟K线) + "max_allowed_drawdown": 0.20 # 20%最大回撤容忍度 + }, + { + "method": "ConsecutiveLossProtection", + # 最大连续亏损次数 + "max_consecutive_losses": 3, + # 锁定时长(3小时) + "stop_duration": 180, + # 回看期(6小时) + "lookback_period": 360 + } + ] + @staticmethod def linear_map(value, from_min, from_max, to_min, to_max): return (value - from_min) / (from_max - from_min) * (to_max - to_min) + to_min @@ -677,11 +696,11 @@ class FreqaiPrimer(IStrategy): self.sell_threshold = max(self.sell_threshold, self.SELL_THRESHOLD_MIN) # 调试日志 - # 处理市场状态预测 - 使用向量化方式避免非日志用途的iloc[-1] + # 处理市场状态预测 if "&*-market_regime" in dataframe.columns: - market_regime_series = dataframe["&*-market_regime"] if len(dataframe) > 0 else pd.Series(2, index=dataframe.index) + market_regime = dataframe["&*-market_regime"].iloc[-1] if len(dataframe) > 0 else 2 - # 基于市场状态调整仓位和止损 - 使用固定标准配置 + # 基于市场状态调整仓位和止损 regime_multipliers = { 0: {"position": 1.5, "stoploss": 0.8, "label": "🟢低波动震荡"}, # 低波动震荡:加大仓位,收紧止损 1: {"position": 1.2, "stoploss": 0.9, "label": "🔵正常趋势"}, # 正常趋势:适中仓位 @@ -690,11 +709,7 @@ class FreqaiPrimer(IStrategy): 4: {"position": 0.5, "stoploss": 1.5, "label": "🔴黑天鹅状态"}, # 黑天鹅状态:最小仓位,最宽止损 } - # 获取最新市场状态用于日志(确认仅用于日志) - market_regime_for_log = market_regime_series.iloc[-1] if len(market_regime_series) > 0 else 2 - - # 使用标准配置(避免非日志用途的iloc[-1]) - regime_config = regime_multipliers[2] # 固定使用标准配置 + regime_config = regime_multipliers.get(market_regime, regime_multipliers[2]) # 应用到策略参数 self.risk_position_multiplier = regime_config["position"] @@ -715,7 +730,7 @@ class FreqaiPrimer(IStrategy): f"&-price_value_divergence: {dataframe['&-price_value_divergence'].iloc[-1]:.6f}, " f"volume_z_score: {dataframe['volume_z_score'].iloc[-1]:.2f}, " f"bb_lowerband: {dataframe['bb_lowerband'].iloc[-1]:.6f}, " - f"market_regime: {market_regime_for_log if '&*-market_regime' in dataframe else 'N/A'}") + f"market_regime: {dataframe['&*-market_regime'].iloc[-1] if '&*-market_regime' in dataframe else 'N/A'}") if not self.stats_logged: logger.info("===== 所有币对的 labels_mean 和 labels_std 汇总 =====") @@ -728,6 +743,85 @@ class FreqaiPrimer(IStrategy): return dataframe + # 入场趋势得分阈值,低于此值禁止入场 + ENTRY_TREND_SCORE_THRESHOLD = 67 + + # 1小时时间框架剧烈拉升检测参数 + # 剧烈拉升检测参数 + H1_RAPID_RISE_LOOKBACK = 200 # 检查最近200根K线 + H1_RAPID_RISE_THRESHOLD = 5 # 价格拉升阈值(百分比) + H1_MAX_CANDLES = 2 # 最多连续检查的K线数量 + H1_UNSTABLE_REGION_THRESHOLD = 60 # 不稳定区域阈值(百分比,超过60%认为是中上部分) + + def detect_h1_rapid_rise(self, dataframe: DataFrame, metadata: dict) -> tuple[bool, float]: + """ + 检测最近K线中是否发生了剧烈拉升情况 + + 参数: + dataframe: 当前时间框架的数据 + metadata: 币对元数据 + + 返回: + tuple: (是否发生剧烈拉升, 当前价格在最近价格范围中的百分比) + """ + pair = metadata.get('pair', 'Unknown') + + try: + # 使用当前时间框架的数据,而不仅仅是1小时框架 + current_timeframe_data = dataframe.copy() + + if len(current_timeframe_data) < self.H1_RAPID_RISE_LOOKBACK: + logger.info(f"[{pair}] K线数据不足{self.H1_RAPID_RISE_LOOKBACK}根,无法进行剧烈拉升检测") + return False, 0.0 + + # 获取最近200根K线 + recent_data = current_timeframe_data[-self.H1_RAPID_RISE_LOOKBACK:] + + # 初始化剧烈拉升标志 + rapid_rise_detected = False + max_cumulative_change = 0 + + # 检查最多连续2根K线内的累计涨幅 + for i in range(1, len(recent_data)): + # 计算从i-n到i的累计涨幅 + for n in range(1, self.H1_MAX_CANDLES + 1): + if i - n >= 0: + start_price = recent_data['close'].iloc[i - n] + end_price = recent_data['close'].iloc[i] + cumulative_change = ((end_price - start_price) / start_price) * 100 + + if cumulative_change > max_cumulative_change: + max_cumulative_change = cumulative_change + + if cumulative_change >= self.H1_RAPID_RISE_THRESHOLD: + rapid_rise_detected = True + break + if rapid_rise_detected: + break + + # 获取当前价格 + current_price = dataframe['close'].iloc[-1] if len(dataframe) > 0 else 0 + + # 计算当前价格在最近价格范围中的位置百分比 + low_price = recent_data['close'].min() + high_price = recent_data['close'].max() + + if high_price > low_price: + price_position_pct = ((current_price - low_price) / (high_price - low_price)) * 100 + else: + price_position_pct = 0.0 + + # 判断当前是否处于不稳固区域(剧烈拉升后价格处于中上部分) + is_unstable_region = rapid_rise_detected and (price_position_pct >= self.H1_UNSTABLE_REGION_THRESHOLD) + + logger.info(f"[{pair}] 剧烈拉升检测 - 最大{self.H1_MAX_CANDLES}根K线累计涨幅: {max_cumulative_change:.2f}%, 阈值: {self.H1_RAPID_RISE_THRESHOLD}%, 不稳固区域: {is_unstable_region}") + + return is_unstable_region, price_position_pct + + except Exception as e: + logger.error(f"[{pair}] 剧烈拉升检测错误: {str(e)}") + return False, 0.0 + def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: pair = metadata.get('pair', 'Unknown') original_length = len(dataframe) @@ -737,6 +831,22 @@ class FreqaiPrimer(IStrategy): logger.info(f"[{pair}] populate_entry_trend 被调用,原始数据长度:{original_length},时间:{dataframe.index[-1]}") # 获取市场趋势得分 trend_score = self.get_market_trend(dataframe=dataframe, metadata=metadata) + + # 检测是否发生剧烈拉升 + is_unstable_region, price_position_pct = self.detect_h1_rapid_rise(dataframe=dataframe, metadata=metadata) + + # 检查趋势得分是否低于阈值,如果是则禁止入场 + if trend_score < self.ENTRY_TREND_SCORE_THRESHOLD: + dataframe['enter_long'] = 0 + logger.info(f"[{pair}] 趋势得分 {trend_score} 低于阈值 {self.ENTRY_TREND_SCORE_THRESHOLD},禁止入场") + return dataframe + + # 计算EMA50和EMA20 - 用于不同市场状态的入场条件 + dataframe["ema50"] = ta.EMA(dataframe, timeperiod=50) + dataframe["ema20"] = ta.EMA(dataframe, timeperiod=20) + price_below_ema50 = dataframe["close"] < dataframe["ema50"] + price_above_ema50 = dataframe["close"] > dataframe["ema50"] + price_below_ema20 = dataframe["close"] < dataframe["ema20"] # 动态调整成交量和 RSI 阈值 volume_z_score_min = 0.5 @@ -754,10 +864,6 @@ class FreqaiPrimer(IStrategy): bearish_signal_aligned = bearish_signal.reindex(dataframe.index, method='ffill').fillna(False) stochrsi_overbought = self.is_stochrsi_overbought(dataframe, period=10, threshold=85) stochrsi_overbought_aligned = stochrsi_overbought.reindex(dataframe.index, method='ffill').fillna(True) - - # 计算EMA50 - 价格必须低于EMA50的硬性要求 - dataframe["ema50"] = ta.EMA(dataframe, timeperiod=50) - price_below_ema50 = dataframe["close"] < dataframe["ema50"] # 检测趋势状态 trend_status = self.detect_trend_status(dataframe, metadata) @@ -768,11 +874,8 @@ class FreqaiPrimer(IStrategy): open_trades = len(self.active_trades) if hasattr(self, 'active_trades') else 0 is_green_channel = (trend_status == "bullish" and open_trades <= 2 and self.GREEN_CHANNEL_ENABLED) - # 获取市场状态并调整入场条件 - 使用向量化方式避免非日志用途的iloc[-1] - market_regime_series = dataframe["&*-market_regime"] if "&*-market_regime" in dataframe.columns else pd.Series(2, index=dataframe.index) - - # 获取最新市场状态用于日志(确认仅用于日志) - market_regime_for_log = market_regime_series.iloc[-1] if len(market_regime_series) > 0 else 2 + # 获取市场状态并调整入场条件 + market_regime = dataframe["&*-market_regime"].iloc[-1] if "&*-market_regime" in dataframe.columns else 2 # 市场状态影响入场严格程度 regime_adjustments = { @@ -783,13 +886,9 @@ class FreqaiPrimer(IStrategy): 4: {"threshold_mult": 0.6, "strict_mult": 1.5}, # 黑天鹅状态:最严格 } - # 使用标准配置(避免非日志用途的iloc[-1]) - regime_adj = regime_adjustments[2] # 固定使用标准配置 + regime_adj = regime_adjustments.get(market_regime, regime_adjustments[2]) - logger.info(f"[{pair}] 市场状态: {market_regime_for_log}, 阈值调整: {regime_adj['threshold_mult']}, 严格度调整: {regime_adj['strict_mult']}") - - # 为每个入场信号定义详细的标签 - entry_tag = "" + logger.info(f"[{pair}] 市场状态: {market_regime}, 阈值调整: {regime_adj['threshold_mult']}, 严格度调整: {regime_adj['strict_mult']}") if is_green_channel: # 🟢 牛市绿色通道:持仓≤2个,25USDT入场,5条件需要满足4个,且价格必须低于EMA50 @@ -802,8 +901,7 @@ class FreqaiPrimer(IStrategy): core_conditions = [cond1, cond2, cond3, cond4, cond5] # 使用向量化操作计算满足条件的数量 satisfied_count_vector = cond1.astype(int) + cond2.astype(int) + cond3.astype(int) + cond4.astype(int) + cond5.astype(int) - buy_condition = (satisfied_count_vector >= 4) & price_below_ema50 # 必须低于EMA50 - entry_tag = "bull_green_channel" + buy_condition = (satisfied_count_vector >= 4) & price_below_ema50 & ~is_unstable_region # 仅在日志中使用最后一行的值 if len(dataframe) > 0: @@ -819,12 +917,11 @@ class FreqaiPrimer(IStrategy): cond5 = (dataframe["stochrsi_k"] < stochrsi_threshold * 1.2) # 放宽STOCHRSI要求 cond6 = pd.Series([True] * len(dataframe), index=dataframe.index) # 取消熊市过滤 cond7 = pd.Series([True] * len(dataframe), index=dataframe.index) # 取消超买过滤 - buy_condition = cond1 & cond2 & cond3 & cond4 & cond5 & cond6 & cond7 & price_below_ema50 # 必须低于EMA50 - entry_tag = "bull_normal" + buy_condition = cond1 & cond2 & cond3 & cond4 & cond5 & cond6 & cond7 & price_below_ema50 & ~is_unstable_region logger.info(f"[{pair}] 🚀 牛市正常通道:持仓{open_trades}>2个,75USDT入场,必须满足全部7个条件") elif trend_status == "bearish": - # 下跌趋势:严格入场条件,只抄底,且价格必须低于EMA50 + # 下跌趋势:严格入场条件,只抄底,且价格必须高于EMA50 cond1 = (dataframe["&-price_value_divergence"] < self.buy_threshold * 0.7) # 严格到0.7倍 cond2 = (dataframe["volume_z_score"] > volume_z_score_threshold * 1.3) # 提高成交量要求 cond3 = (dataframe["rsi"] < rsi_threshold * 0.8) # 严格RSI要求 @@ -832,12 +929,11 @@ class FreqaiPrimer(IStrategy): cond5 = (dataframe["stochrsi_k"] < stochrsi_threshold * 0.8) # 严格STOCHRSI要求 cond6 = ~bearish_signal_aligned # 保持熊市过滤 cond7 = ~stochrsi_overbought_aligned # 保持超买过滤 - buy_condition = cond1 & cond2 & cond3 & cond4 & cond5 & cond6 & cond7 & price_below_ema50 # 必须低于EMA50 - entry_tag = "bearish" + buy_condition = cond1 & cond2 & cond3 & cond4 & cond5 & cond6 & cond7 & price_above_ema50 & ~is_unstable_region logger.info(f"[{pair}] 📉 下跌趋势策略:严格入场条件") else: # ranging - # 震荡趋势:使用原策略,且价格必须低于EMA50 + # 震荡趋势:close必须低于ema20,且高于ema50 cond1 = (dataframe["&-price_value_divergence"] < self.buy_threshold) cond2 = (dataframe["volume_z_score"] > volume_z_score_threshold) cond3 = (dataframe["rsi"] < rsi_threshold) @@ -845,55 +941,12 @@ class FreqaiPrimer(IStrategy): cond5 = (dataframe["stochrsi_k"] < stochrsi_threshold) cond6 = ~bearish_signal_aligned cond7 = ~stochrsi_overbought_aligned - buy_condition = cond1 & cond2 & cond3 & cond4 & cond5 & cond6 & cond7 & price_below_ema50 # 必须低于EMA50 - entry_tag = "ranging" - logger.info(f"[{pair}] ⚖️ 震荡趋势策略:标准入场条件") + buy_condition = cond1 & cond2 & cond3 & cond4 & cond5 & cond6 & cond7 & price_below_ema20 & price_above_ema50 & ~is_unstable_region + logger.info(f"[{pair}] ⚖️ 震荡趋势策略:close必须低于ema20,且高于ema50") # 绿色通道和趋势状态的条件已经设置好buy_condition conditions.append(buy_condition) - # 🎯 计算信号强度评分 (0-100分) 并作为tag传递 - signal_strength = 0.0 - if len(dataframe) > 0: - # 基础评分:趋势得分影响 - base_score = trend_score * 0.4 # 趋势得分占40% - - # 条件满足评分 - 使用向量化操作避免非日志用途的iloc[-1] - satisfied_count_vector = cond1.astype(int) + cond2.astype(int) + cond3.astype(int) + cond4.astype(int) + cond5.astype(int) - - # 创建不同趋势状态的掩码 - green_mask = buy_condition & is_green_channel - bullish_mask = buy_condition & (trend_status == "bullish") - bearish_mask = buy_condition & (trend_status == "bearish") - ranging_mask = buy_condition & (trend_status not in ["bullish", "bearish"]) - - # 初始化信号强度向量 - signal_strength_vector = pd.Series(0.0, index=dataframe.index) - - # 绿色通道:基础分60 + 条件满足加分 - if green_mask.any(): - signal_strength_vector.loc[green_mask] = 60 + (satisfied_count_vector[green_mask] * 8) + base_score - - # 牛市正常:基础分50 + 条件满足加分 - if bullish_mask.any(): - signal_strength_vector.loc[bullish_mask] = 50 + (satisfied_count_vector[bullish_mask] * 7) + (base_score * 1.2) - - # 熊市:基础分40 + 超跌加分 - if bearish_mask.any(): - oversold_bonus = np.maximum(0, (100 - trend_score) * 0.3) - signal_strength_vector.loc[bearish_mask] = 40 + (satisfied_count_vector[bearish_mask] * 8) + oversold_bonus - - # 震荡市:基础分45 + 条件满足加分 - if ranging_mask.any(): - signal_strength_vector.loc[ranging_mask] = 45 + (satisfied_count_vector[ranging_mask] * 8) + base_score - - # 限制在0-100分范围 - signal_strength_vector = np.clip(signal_strength_vector, 0, 100) - - # 存储信号强度到dataframe - dataframe.loc[buy_condition, 'signal_strength'] = signal_strength_vector[buy_condition] - dataframe.loc[buy_condition, 'immediate_entry'] = signal_strength_vector[buy_condition] >= 75 # 75分以上立即入场 - # 调试日志 - 仅在日志中使用最后一行的值 if len(dataframe) > 0: divergence_value = dataframe["&-price_value_divergence"].iloc[-1] @@ -903,12 +956,17 @@ class FreqaiPrimer(IStrategy): bb_close_value = dataframe["close"].iloc[-1] bb_lower_value = dataframe["bb_lowerband"].iloc[-1] bb_upper_value = dataframe["bb_upperband"].iloc[-1] - ema50_value = dataframe["ema50"].iloc[-1] # 输出关键指标以便调试 - logger.info(f"[{pair}] 📊 关键指标 - 偏离度: {divergence_value:.6f}, RSI: {rsi_value:.2f}, STOCHRSI: {stochrsi_value:.2f}, 价格: {bb_close_value:.4f}, EMA50: {ema50_value:.4f}, 价格低于EMA50: {bb_close_value < ema50_value}") + logger.info(f"[{pair}] 📊 关键指标 - 偏离度: {divergence_value:.6f}, RSI: {rsi_value:.2f}, STOCHRSI: {stochrsi_value:.2f}, 价格: {bb_close_value:.4f}, 下轨: {bb_lower_value:.4f}, 上轨: {bb_upper_value:.4f}") logger.info(f"[{pair}] 🔍 买入阈值 - 偏离度: {self.buy_threshold:.6f}, RSI: {rsi_threshold:.2f}, STOCHRSI: {stochrsi_threshold:.2f}") + # 记录剧烈拉升检测结果 + if is_unstable_region: + logger.warning(f"[{pair}] ⚠️ 检测到剧烈拉升,当前价格位置: {price_position_pct:.2f}%,处于不稳固区域,禁止入场") + else: + logger.info(f"[{pair}] ✅ 价格走势正常,当前价格位置: {price_position_pct:.2f}%") + # 获取条件结果的最后一行值(仅用于日志) cond1_last = cond1.iloc[-1] cond2_last = cond2.iloc[-1] @@ -951,101 +1009,6 @@ class FreqaiPrimer(IStrategy): if conditions: combined_condition = reduce(lambda x, y: x & y, conditions) dataframe.loc[combined_condition, 'enter_long'] = 1 - dataframe.loc[combined_condition, 'entry_tag'] = entry_tag - - # 🎯 记录实际入场信号到Redis(只在enter_long=1时) - enter_long_indices = dataframe[dataframe['enter_long'] == 1].index - if len(enter_long_indices) > 0: - # 获取最后一根K线的信号数据(避免重复记录) - last_idx = enter_long_indices[-1] - - # 只在live模式下记录Redis信号 - current_mode = self.config.get('runmode') - if current_mode == RunMode.LIVE: - try: - # 获取容器名称和环境变量 - container_name = os.getenv('CONTAINER_NAME', socket.gethostname()) - - # 获取进程启动时间戳(全局有效) - if not hasattr(self, '_start_timestamp'): - self._start_timestamp = int(time.time() * 1000) - - # 将币对名从BTC/USDT格式转换为BTC-USDT格式 - pair_redis = pair.replace('/', '-') - - # 获取当前时间戳(毫秒) - timestamp_ms = int(time.time() * 1000) - - # 构建Redis key: ${CONTAINER_NAME}_{startTimeStamp}_{币对名}_entry_{时间戳} - redis_key = f"{container_name}_{self._start_timestamp}_{pair_redis}_entry_{timestamp_ms}" - - # 获取Redis客户端 - redis_client = self._get_redis_client() - if redis_client: - # 构建详细的条件分析 - detailed_conditions = [] - for name, value, operator, threshold, result in conditions_summary: - condition_detail = { - 'name': name, - 'value': float(value) if value is not None else None, - 'threshold': float(threshold) if threshold is not None else None, - 'operator': operator, - 'satisfied': bool(result) - } - detailed_conditions.append(condition_detail) - - # 准备要存储的详细信号数据 - signal_data = { - 'signal_strength': float(dataframe.loc[last_idx, 'signal_strength']) if 'signal_strength' in dataframe.columns else 0.0, - 'entry_tag': str(dataframe.loc[last_idx, 'entry_tag']) if 'entry_tag' in dataframe.columns else str(entry_tag), - 'trend_score': float(trend_score), - 'market_regime': str(market_regime), - 'timestamp': timestamp_ms, - 'pair': str(pair), - 'hostname': str(container_name), - 'immediate_entry': bool(dataframe.loc[last_idx, 'immediate_entry']) if 'immediate_entry' in dataframe.columns else False, - 'conditions_analysis': { - 'total_conditions': len(conditions_summary), - 'satisfied_count': sum(1 for _, _, _, _, result in conditions_summary if result), - 'failed_count': sum(1 for _, _, _, _, result in conditions_summary if not result), - 'detailed_conditions': detailed_conditions - }, - 'current_market_data': { - 'close_price': float(dataframe.loc[last_idx, 'close']), - 'ema50': float(dataframe.loc[last_idx, 'ema50']) if 'ema50' in dataframe.columns else None, - 'ema200': float(dataframe.loc[last_idx, 'ema200']) if 'ema200' in dataframe.columns else None, - 'bb_lower': float(dataframe.loc[last_idx, 'bb_lowerband']) if 'bb_lowerband' in dataframe.columns else None, - 'bb_upper': float(dataframe.loc[last_idx, 'bb_upperband']) if 'bb_upperband' in dataframe.columns else None, - 'rsi': float(dataframe.loc[last_idx, 'rsi']) if 'rsi' in dataframe.columns else None, - 'volume': float(dataframe.loc[last_idx, 'volume']) if 'volume' in dataframe.columns else None - }, - 'strategy_parameters': { - 'buy_threshold_min': float(self.buy_threshold_min), - 'buy_threshold_max': float(self.buy_threshold_max), - 'trend_bullish_threshold': int(self.TREND_BULLISH_THRESHOLD), - 'trend_bearish_threshold': int(self.TREND_BEARISH_THRESHOLD), - 'green_channel_discount': float(self.GREEN_CHANNEL_DISCOUNT), - 'entry_discount_bull_normal': float(self.entry_discount_bull_normal.value), - 'entry_discount_ranging': float(self.entry_discount_ranging.value), - 'entry_discount_bearish': float(self.entry_discount_bearish.value) - } - } - - # 将信号数据序列化为JSON字符串 - signal_json = json.dumps(signal_data, ensure_ascii=False, indent=2) - - # 存储到Redis,设置300天过期时间(25920000秒) - redis_client.setex(redis_key, 25920000, signal_json) - - logger.info(f"[Redis] ✅ Live模式 - 实际入场信号已记录: {redis_key}, 信号强度: {signal_data['signal_strength']:.2f}, 趋势: {signal_data['entry_tag']}") - - else: - logger.debug(f"[Redis] ⚠️ Redis客户端不可用,跳过记录: {pair}") - - except Exception as e: - logger.error(f"[Redis] ❌ Live模式 - 记录入场信号失败: {e}") - else: - logger.debug(f"[Redis] ℹ️ 非Live模式({current_mode}),跳过Redis记录: {pair}") # 输出每个条件的状态 logger.info(f"[{pair}] === 买入条件检查 ===") @@ -1393,62 +1356,98 @@ class FreqaiPrimer(IStrategy): current_entry_profit: float, current_exit_profit: float, **kwargs) -> float | None | tuple[float | None, str | None]: """ - 统一标准的仓位调整策略: - - 移除所有趋势状态判断和趋势得分依赖 - - 基于固定阈值和递进系数的加仓逻辑 - - 每次加仓阈值递增:初次阈值 × (递进系数^加仓次数) + 动态调整仓位:支持加仓、减仓、追踪止损和最大持仓时间限制 + 参数: + - trade: 当前交易对象 + - current_time: 当前时间 + - current_rate: 当前价格 + - current_profit: 当前总盈利 + - min_stake: 最小下注金额 + - max_stake: 最大下注金额 + - current_entry_rate: 当前入场价格 + - current_exit_rate: 当前退出价格 + - current_entry_profit: 当前入场盈利 + - current_exit_profit: 当前退出盈利 + 返回: + - 调整金额(正数为加仓,负数为减仓)或 None """ pair = trade.pair + dataframe = self.dp.get_pair_dataframe(pair, self.timeframe) + trend_score = self.get_market_trend(dataframe=dataframe, metadata={'pair': pair}) hold_time = (current_time - trade.open_date_utc).total_seconds() / 60 profit_ratio = (current_rate - trade.open_rate) / trade.open_rate - # 统一参数设置(移除趋势状态判断) - max_entry_adjustments = self.MAX_ENTRY_POSITION_ADJUSTMENT # 最大加仓次数 - exit_position_ratio = self.EXIT_POSITION_RATIO # 减仓阈值 - trailing_stop_start = self.TRAILING_STOP_START # 追踪止损启动阈值 - trailing_stop_distance = self.TRAILING_STOP_DISTANCE # 追踪止损距离 - - # 统一入场金额(移除绿色通道判断) - initial_stake_amount = 75.0 # 固定75USDT入场 - - logger.info(f"{pair} 统一仓位管理: max_entries={max_entry_adjustments}, initial_stake={initial_stake_amount:.2f}") + # 检测趋势状态(必须先定义才能使用) + trend_status = self.detect_trend_status(dataframe, {'pair': pair}) + logger.info(f"{pair} 当前趋势状态: {trend_status}") - # 🔢 计算递进加仓阈值 - add_count = trade.nr_of_successful_entries - 1 # 已加仓次数 + # 检测当前持仓订单数量 + open_trades = len(self.active_trades) if hasattr(self, 'active_trades') else 0 - # 计算当前加仓阈值:config中的ADD_POSITION_THRESHOLD × (递进系数^加仓次数) - current_add_threshold = self.ADD_POSITION_THRESHOLD * (self.ADD_PROGRESSION_FACTOR ** add_count) + # 牛市绿色通道判断:持仓≤2个且牛市趋势,且绿色通道开关开启 + is_green_channel = (trend_status == "bullish" and open_trades <= 2 and self.GREEN_CHANNEL_ENABLED) - # 计算从上一次加仓以来的跌幅(使用正确的基准) - if add_count > 0: - # 有加仓记录,使用最后一次加仓价作为基准 - last_entry_price = trade.orders[-1].safe_price if trade.orders else trade.open_rate - drop_since_last_entry = (current_rate - last_entry_price) / last_entry_price - drop_baseline = drop_since_last_entry - logger.info(f"{pair} 第{add_count}次加仓后跌幅: {drop_since_last_entry*100:.2f}% (基于上次加仓价)") + # 根据绿色通道调整入场金额 + if is_green_channel: + initial_stake_amount = 25.0 # 绿色通道:25USDT入场 + logger.info(f"{pair} 🟢 牛市绿色通道:25USDT入场,当前持仓{open_trades}个") else: - # 首次加仓,使用初始入场价 - drop_baseline = profit_ratio - logger.info(f"{pair} 首次加仓跌幅: {profit_ratio*100:.2f}% (基于初始入场价)") - - # 统一加仓逻辑(增加冷却期检查) - if trade.nr_of_successful_entries <= max_entry_adjustments + 1: - # 检查加仓冷却期(至少10分钟) - if trade.orders: - last_order_time = trade.orders[-1].order_date_utc - time_since_last_add = (current_time - last_order_time).total_seconds() / 60 - if time_since_last_add < 10: # 10分钟冷却期 - logger.info(f"{pair} 加仓冷却期中,{10 - time_since_last_add:.1f}分钟后可再次加仓") - return None + initial_stake_amount = 75.0 # 正常通道:75USDT入场 + logger.info(f"{pair} 首次入场金额: {initial_stake_amount:.2f}, 当前持仓金额: {trade.stake_amount:.2f}, " + f"加仓次数: {trade.nr_of_successful_entries - 1}, 趋势得分: {trend_score:.2f}") + + # 根据趋势状态调整仓位管理参数 + if trend_status == "bullish": + # 上涨趋势:积极加仓,放宽止盈 + max_entry_adjustments = min(self.MAX_ENTRY_POSITION_ADJUSTMENT + 1, 5) # 允许更多加仓 + add_position_threshold = self.ADD_POSITION_THRESHOLD * 1.3 # 更宽松的加仓条件 + exit_position_ratio = self.EXIT_POSITION_RATIO * 1.4 # 更高的止盈目标 + trailing_stop_start = self.TRAILING_STOP_START * 1.2 # 更高的启动阈值 + trailing_stop_distance = self.TRAILING_STOP_DISTANCE * 1.5 # 更大的回撤容忍 + logger.info(f"{pair} 🚀 上涨趋势仓位管理参数: max_entries={max_entry_adjustments}, add_thresh={add_position_threshold:.4f}, exit_ratio={exit_position_ratio:.2%}") - if drop_baseline <= current_add_threshold and hold_time > 5: # 使用正确跌幅基准 - # 新的加仓逻辑:基于标准入场金额一半(37.5)的递进倍数 - base_add_amount = initial_stake_amount / 2 # 37.5 USDT - multipliers = [1, 4, 8, 16] # 递进倍数序列 [首次, 第二次, 第三次, 第四次] + elif trend_status == "bearish": + # 下跌趋势:谨慎加仓,严格止盈 + max_entry_adjustments = max(self.MAX_ENTRY_POSITION_ADJUSTMENT - 1, 1) # 减少加仓次数 + add_position_threshold = self.ADD_POSITION_THRESHOLD * 0.7 # 更严格的加仓条件 + exit_position_ratio = self.EXIT_POSITION_RATIO * 0.8 # 更低的止盈目标 + trailing_stop_start = self.TRAILING_STOP_START * 0.8 # 更低的启动阈值 + trailing_stop_distance = self.TRAILING_STOP_DISTANCE * 0.7 # 更严格的止损 + logger.info(f"{pair} 📉 下跌趋势仓位管理参数: max_entries={max_entry_adjustments}, add_thresh={add_position_threshold:.4f}, exit_ratio={exit_position_ratio:.2%}") + + else: # ranging + # 震荡趋势:使用标准参数 + max_entry_adjustments = self.MAX_ENTRY_POSITION_ADJUSTMENT + add_position_threshold = self.ADD_POSITION_THRESHOLD + exit_position_ratio = self.EXIT_POSITION_RATIO + trailing_stop_start = self.TRAILING_STOP_START + trailing_stop_distance = self.TRAILING_STOP_DISTANCE + logger.info(f"{pair} ⚖️ 震荡趋势仓位管理参数: max_entries={max_entry_adjustments}, add_thresh={add_position_threshold:.4f}, exit_ratio={exit_position_ratio:.2%}") + + # 加仓逻辑 + if trade.nr_of_successful_entries <= max_entry_adjustments + 1: + # 动态调整加仓阈值 + if trend_status == "bullish": + add_threshold = 90 - 20 * (trend_score / 100) # 上涨趋势下更宽松 + elif trend_status == "bearish": + add_threshold = 70 - 30 * (trend_score / 100) # 下跌趋势下更谨慎 + else: + add_threshold = 80 - 30 * (trend_score / 100) # 震荡趋势标准 + if profit_ratio <= add_position_threshold and hold_time > 5 and trend_score <= add_threshold: + add_count = trade.nr_of_successful_entries - 1 + + # 根据趋势状态调整加仓倍数 + if trend_status == "bullish": + multipliers = [1.5, 3, 6] # 上涨趋势下更保守的加仓 + elif trend_status == "bearish": + multipliers = [1, 2, 4] # 下跌趋势下更激进的加仓(抄底) + else: + multipliers = [2, 4, 8] # 震荡趋势标准加仓 + if add_count < len(multipliers): multiplier = multipliers[add_count] - add_amount = base_add_amount * multiplier + add_amount = initial_stake_amount * multiplier if min_stake is not None and add_amount < min_stake: logger.warning(f"{pair} 加仓金额 {add_amount:.2f} 低于最小下注金额 {min_stake:.2f}") @@ -1456,51 +1455,47 @@ class FreqaiPrimer(IStrategy): if add_amount > max_stake: add_amount = max_stake - logger.info(f"{pair} 第 {add_count + 1} 次加仓触发: " - f"跌幅 {profit_ratio*100:.2f}% <= 阈值 {current_add_threshold*100:.2f}%, " - f"加仓金额 {add_amount:.2f} (基于37.5×{multiplier})") - return (add_amount, f"Add {add_count + 1}: Drop {profit_ratio*100:.2f}% <= {current_add_threshold*100:.2f}%, add {add_amount:.2f} (37.5×{multiplier})") + logger.info(f"{pair} 趋势状态: {trend_status}, 价格下跌 {profit_ratio*100:.2f}%,触发第 {add_count + 1} 次加仓 {add_amount:.2f}") + return (add_amount, f"Trend: {trend_status}, Price dropped {profit_ratio*100:.2f}%, add {add_amount:.2f}") - # 统一减仓逻辑(移除趋势判断) + # 减仓逻辑 if profit_ratio >= exit_position_ratio: - reduce_factor = 1.0 # 统一全部减仓 + # 根据趋势状态调整减仓比例 + if trend_status == "bullish": + reduce_factor = 0.5 # 上涨趋势下只减仓50% + elif trend_status == "bearish": + reduce_factor = 1.0 # 下跌趋势下全部减仓 + else: + reduce_factor = 0.8 # 震荡趋势下减仓80% + reduce_amount = -trade.stake_amount * reduce_factor - logger.info(f"{pair} 利润 {profit_ratio*100:.2f}% 达到减仓阈值 {exit_position_ratio*100:.2f}%,减仓 {abs(reduce_amount):.2f}") - return (reduce_amount, f"Profit {profit_ratio*100:.2f}% >= {exit_position_ratio*100:.2f}%, reduce {abs(reduce_amount):.2f}") + logger.info(f"{pair} 趋势状态: {trend_status}, 利润 {profit_ratio*100:.2f}%,减仓 {abs(reduce_amount):.2f} ({reduce_factor*100:.0f}%)") + return (reduce_amount, f"Trend: {trend_status}, Profit {profit_ratio*100:.2f}%, reduce {abs(reduce_amount):.2f}") - # 统一追踪止损逻辑(移除趋势判断) + # 追踪止损逻辑 if profit_ratio >= trailing_stop_start and not self.trailing_stop_enabled: self.trailing_stop_enabled = True trade.adjust_min_max_rates(current_rate, current_rate) - logger.info(f"{pair} 价格上涨 {profit_ratio*100:.2f}% 超过 {trailing_stop_start*100:.1f}%,启动追踪止损") + logger.info(f"{pair} 趋势状态: {trend_status}, 价格上涨超过 {trailing_stop_start*100:.1f}%,启动追踪止损") return None if self.trailing_stop_enabled: max_rate = trade.max_rate or current_rate trailing_stop_price = max_rate * (1 - trailing_stop_distance) if current_rate < trailing_stop_price: - logger.info(f"{pair} 价格回落至 {trailing_stop_price:.6f},触发全部卖出") - return (-trade.stake_amount, f"Trailing stop at {trailing_stop_price:.6f}") + logger.info(f"{pair} 趋势状态: {trend_status}, 价格回落至 {trailing_stop_price:.6f},触发全部卖出") + return (-trade.stake_amount, f"Trend: {trend_status}, Trailing stop at {trailing_stop_price:.6f}") trade.adjust_min_max_rates(current_rate, trade.min_rate) return None return None def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float, - time_in_force: str, current_time: datetime, entry_tag: str | None = None, **kwargs) -> bool: + time_in_force: str, current_time: datetime, **kwargs) -> bool: # 调试日志:记录输入参数 logger.info(f"[{pair}] confirm_trade_entry called with rate={rate}, type(rate)={type(rate)}, " f"amount={amount}, order_type={order_type}, time_in_force={time_in_force}") - # 🔒 策略启动保护:运行未满10分钟禁止交易 - if not hasattr(self, 'strategy_start_time'): - self.strategy_start_time = current_time - - strategy_run_time = (current_time - self.strategy_start_time).total_seconds() / 60 - if strategy_run_time < 10: # 10分钟启动保护 - logger.info(f"[{pair}] 策略启动保护中,运行{strategy_run_time:.1f}分钟 < 10分钟,禁止交易") - return False - # 检查 rate 是否有效 if not isinstance(rate, (float, int)) or rate is None: logger.error(f"[{pair}] Invalid rate value: {rate} (type: {type(rate)}). Skipping trade entry.") @@ -1533,196 +1528,34 @@ class FreqaiPrimer(IStrategy): rate: float, time_in_force: str, exit_reason: str, current_time: datetime, **kwargs) -> bool: adjusted_rate = rate * (1 + 0.0025) - - # 增强exit tag显示,包含更多信息 - trade_profit = trade.calc_profit_ratio(rate) - hold_duration = (current_time - trade.open_date_utc).total_seconds() / 3600 # 小时 - - # 为不同exit原因添加详细tag - detailed_exit_reason = f"{exit_reason}_profit{trade_profit:.1%}_hold{hold_duration:.1f}h" - - logger.info(f"[{pair}] 退出交易,原因:{detailed_exit_reason}, 原始利润:{trade_profit:.2%}, 调整后卖出价:{adjusted_rate:.6f}") - - # 将详细信息添加到trade对象,便于UI显示 - if hasattr(trade, 'exit_reason_detailed'): - trade.exit_reason_detailed = detailed_exit_reason - + logger.info(f"[{pair}] 退出交易,原因:{exit_reason}, 原始利润:{trade.calc_profit_ratio(rate):.2%},"f"调整后卖出价:{adjusted_rate:.6f}") return True - def custom_roi(self, trade: Trade, current_profit: float, current_time: datetime, trade_dur: int, - current_rate: float = None, min_stake: float | None = None, max_stake: float | None = None) -> dict: - """ - 智能ROI策略:平衡趋势盈利与横盘出场 - - 加仓未完成时完全禁用ROI机制(保留趋势交易潜力) - - 小盈利状态下快速出场(解决横盘问题) - - 大盈利状态下保留趋势空间(避免错过5%+涨幅) - - 基于盈利幅度智能分层管理 - """ - pair = trade.pair - logger.info(f"[{pair}] 计算自定义 ROI,当前盈利: {current_profit:.2%}, 持仓时间: {trade_dur} 分钟") - - # 🎯 小盈利快速出场:仅针对微利横盘(不影响大盈利趋势交易) - if current_profit > 0 and current_profit < 0.02 and trade_dur >= 60: - small_profit_roi = {0: 0.005} # 0.5%快速止盈,避免横盘消耗时间 - logger.info(f"[{pair}] 小盈利快速出场:盈利{current_profit:.2%},持仓{trade_dur}分钟,0.5%止盈") - return small_profit_roi - - # 🎯 横盘止损:小幅亏损且长时间持仓(避免深套) - if current_profit < 0 and abs(current_profit) < 0.015 and trade_dur >= 90: - small_loss_exit = {0: -0.015} # -1.5%止损出场 - logger.info(f"[{pair}] 横盘止损:亏损{current_profit:.2%},持仓{trade_dur}分钟,-1.5%止损") - return small_loss_exit - - # 检查加仓次数是否用完 - filled_entries = trade.select_filled_orders(trade.entry_side) - add_count = len(filled_entries) - 1 # 减去首笔入场 - - if add_count < MAX_ENTRY_POSITION_ADJUSTMENT: - # 加仓未完成,完全禁用ROI机制 - disabled_roi = { - 0: 1.0, # 极高值,确保不会触发 - 999999: 1.0 - } - logger.info(f"[{pair}] 加仓次数 {add_count}/{MAX_ENTRY_POSITION_ADJUSTMENT} 未完成,禁用ROI机制") - return disabled_roi - - # 加仓完成后才考虑ROI - if current_profit < 0: - # 亏损状态下使用保守ROI(缩短时间) - conservative_roi = { - 0: 0.05, # 收紧到5% - 30: 0.03, # 30分钟内3% - 60: 0.01, # 60分钟内1% - 90: 0.0 # 90分钟强制出场 - } - logger.info(f"[{pair}] 加仓完成但亏损 {current_profit:.2%},使用保守ROI: {conservative_roi}") - return conservative_roi - - # 🎯 分层盈利管理:根据盈利幅度智能调整ROI - dataframe = self.dp.get_pair_dataframe(pair=pair, timeframe=self.timeframe) - dataframe = self.populate_indicators(dataframe, {'pair': pair}) - - divergence = dataframe["&-price_value_divergence"].iloc[-1] if "&-price_value_divergence" in dataframe else 0 - rsi = dataframe["rsi"].iloc[-1] if "rsi" in dataframe else 50 - - # 🚀 大盈利保护:盈利>5%时延长持仓时间,捕获趋势 - if current_profit >= 0.05: - trend_following_roi = { - 0: 0.15, # 15% - 给趋势足够空间 - 180: 0.10, # 3小时后10% - 360: 0.05, # 6小时后5% - 720: 0.02, # 12小时后2% - 1440: 0.0 # 24小时后出场 - } - logger.info(f"[{pair}] 🚀 大盈利{current_profit:.2%},启用趋势跟踪ROI: {trend_following_roi}") - return trend_following_roi - - # 📈 中等盈利:2%-5%之间,平衡出场 - elif 0.02 <= current_profit < 0.05: - moderate_roi = { - 0: 0.05, # 5% - 保留一定空间 - 90: 0.03, # 1.5小时后3% - 180: 0.015, # 3小时后1.5% - 360: 0.0 # 6小时后出场 - } - logger.info(f"[{pair}] 📈 中等盈利{current_profit:.2%},使用平衡ROI: {moderate_roi}") - return moderate_roi - - # 🎯 小盈利:0-2%之间,使用动态调整 - else: - # 小盈利状态下使用较紧的ROI,但保留趋势空间 - divergence_factor = self.linear_map(divergence, -0.1, 0.1, 1.2, 0.8) - rsi_factor = self.linear_map(rsi, 30, 70, 1.2, 0.8) - - # 根据市场状态动态调整,但给趋势留空间 - base_roi = 0.025 # 基础2.5%,比之前的激进设置宽松 - dynamic_factor = min(divergence_factor * rsi_factor, 1.5) - - small_profit_roi = { - 0: min(base_roi * dynamic_factor, 0.035), # 最大3.5% - 60: 0.02, # 1小时后2% - 120: 0.01, # 2小时后1% - 180: 0.0 # 3小时后出场 - } - logger.info(f"[{pair}] 🎯 小盈利{current_profit:.2%},使用动态ROI: {small_profit_roi}") - return small_profit_roi - def custom_entry_price(self, pair: str, trade: Trade | None, current_time: datetime, proposed_rate: float, - entry_tag: str | None, side: str, **kwargs) -> float: - """根据入场标签动态调整入场价格 - - 基于入场信号的判定条件,为不同的市场状态提供不同的价格折扣: - - bull_green_channel: 牛市绿色通道(固定折扣,不走Hyperopt) - - bull_normal: 牛市正常通道(Hyperopt优化) - - ranging: 震荡市(Hyperopt优化) - - bearish: 熊市(Hyperopt优化) - - Args: - pair: 交易对 - trade: 交易对象 - current_time: 当前时间 - proposed_rate: 建议价格 - entry_tag: 入场信号标签 - side: 交易方向 - **kwargs: 其他参数 - - Returns: - 调整后的入场价格 - """ - # 根据入场标签获取对应的折扣率 - if entry_tag == "bull_green_channel": - discount = self.GREEN_CHANNEL_DISCOUNT # 牛市绿色通道固定折扣 - logger.info(f"[{pair}] 🟢 牛市绿色通道入场,固定折扣: {discount*100:.2f}%") - elif entry_tag == "bull_normal": - discount = float(self.entry_discount_bull_normal.value) # 牛市正常通道折扣(Hyperopt优化) - logger.info(f"[{pair}] 🚀 牛市正常通道入场,折扣率: {discount*100:.2f}%") - elif entry_tag == "ranging": - discount = float(self.entry_discount_ranging.value) # 震荡市折扣(Hyperopt优化) - logger.info(f"[{pair}] ⚖️ 震荡市入场,折扣率: {discount*100:.2f}%") - elif entry_tag == "bearish": - discount = float(self.entry_discount_bearish.value) # 熊市折扣(Hyperopt优化) - logger.info(f"[{pair}] 📉 熊市入场,折扣率: {discount*100:.2f}%") - else: - discount = 0.0 # 无折扣 - logger.info(f"[{pair}] 无入场标签,使用原价 {proposed_rate:.6f}") - - adjusted_rate = proposed_rate * (1 - discount) - if discount > 0: - logger.info(f"[{pair}] 入场标签: {entry_tag}, 原价: {proposed_rate:.6f}, 调整后: {adjusted_rate:.6f}, 折扣: {discount*100:.2f}%") - - return adjusted_rate + entry_tag: str | None, side: str, **kwargs) -> float: + # 增强防御性检查,确保价格永远不会为零 + if not isinstance(proposed_rate, (int, float)) or not np.isfinite(proposed_rate) or proposed_rate <= 1e-10: + logger.warning(f"[{pair}] 无效的 proposed_rate: {proposed_rate},回退到 close 价格") + dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) + if len(dataframe) > 0 and 'close' in dataframe and not pd.isna(dataframe['close'].iloc[-1]) and dataframe['close'].iloc[-1] > 1e-10: + proposed_rate = float(dataframe['close'].iloc[-1]) + else: + proposed_rate = 0.01 # 安全兜底,确保不为零 - def custom_exit(self, pair: str, trade: Trade, - current_time: datetime, current_rate: float, - current_profit: float, **kwargs) -> str | bool | None: - """ - 自定义退出信号逻辑:当加仓次数 >= 2时,拒绝一切exit信号(止损除外) + # 确保返回的价格绝对不会为零,使用更大的最小值 + adjusted_rate = max(proposed_rate * 0.995, 1e-6) # 至少 0.000001 - :param pair: 当前分析的币对 - :param trade: 交易对象 - :param current_time: 当前时间 - :param current_rate: 当前价格 - :param current_profit: 当前利润(比例) - :param kwargs: 其他参数 - :return: 返回None或False表示不退出,返回字符串或True表示退出 - """ - # 获取加仓次数(包括初始入场) - entry_count = trade.nr_of_successful_entries - - if entry_count >= 3: # 初始入场 + 2次加仓 = 3次 - # 当加仓次数 >= 2时(总共3次入场),拒绝所有退出信号(止损除外) - logger.info(f"[{pair}] 加仓次数 {entry_count - 1} >= 2(总入场{entry_count}次),拒绝退出信号(止损除外)") - return None # 阻止所有非止损的退出 - - # 加仓次数 < 2时,允许正常退出 - return None + # 再次检查,确保返回值绝对不为零 + if adjusted_rate <= 0: + adjusted_rate = 0.01 + logger.error(f"[{pair}] 紧急修复:adjusted_rate 为零或负值,强制设置为 0.01") + + logger.info(f"[{pair}] 自定义买入价:{adjusted_rate:.6f}(原价:{proposed_rate:.6f})") + return adjusted_rate def custom_exit_price(self, pair: str, trade: Trade, current_time: datetime, proposed_rate: float, current_profit: float, exit_tag: str | None, **kwargs) -> float: - """ - 自定义退出价格逻辑 - """ adjusted_rate = proposed_rate * (1 + 0.0025) logger.info(f"[{pair}] 自定义卖出价:{adjusted_rate:.6f}(原价:{proposed_rate:.6f})") return adjusted_rate @@ -2004,12 +1837,11 @@ class FreqaiPrimer(IStrategy): self.cleanup_local_cache() # 打印缓存统计 - stats = self._local_cache_stats - total_requests = stats['hits'] + stats['misses'] - if total_requests > 0: - local_hit_rate = (stats['hits'] / total_requests) * 100 - logger.info(f"📊 缓存性能统计: 本地命中率 {local_hit_rate:.1f}%, " - f"计算次数: {stats['computes']}") + stats = self.get_cache_stats() + if stats['total_requests'] > 0: + logger.info(f"📊 缓存性能统计: 本地命中率 {stats['local_hit_rate']:.1f}%, " + f"Redis命中率 {stats['redis_hit_rate']:.1f}%, " + f"总命中率 {stats['overall_hit_rate']:.1f}%") def bot_start(self, **kwargs) -> None: """ @@ -2028,17 +1860,6 @@ class FreqaiPrimer(IStrategy): # 初始化循环计数器 self._loop_counter = 0 - - # 🎯 测试Redis连接 - redis_client = self._get_redis_client() - if redis_client: - try: - redis_client.ping() - logger.info("✅ Redis连接测试成功 - 入场信号记录已启用") - except Exception as e: - logger.error(f"❌ Redis连接测试失败: {e}") - else: - logger.warning("⚠️ Redis客户端初始化失败,入场信号将不会记录到Redis") def bot_stop(self, **kwargs) -> None: """ @@ -2052,62 +1873,16 @@ class FreqaiPrimer(IStrategy): self.cleanup_local_cache() # 打印最终统计 - stats = self._local_cache_stats - total_requests = stats['hits'] + stats['misses'] - if total_requests > 0: - hit_rate = (stats['hits'] / total_requests) * 100 - logger.info(f"📊 最终缓存统计 - 总请求: {total_requests}, " - f"本地命中: {stats['hits']}, " + stats = self.get_cache_stats() + if stats['total_requests'] > 0: + logger.info(f"📊 最终缓存统计 - 总请求: {stats['total_requests']}, " + f"本地命中: {stats['local_hits']}, " + f"Redis命中: {stats['redis_hits']}, " f"计算次数: {stats['computes']}, " - f"总命中率: {hit_rate:.1f}%") + f"总命中率: {stats['overall_hit_rate']:.1f}%") # 注意:由于使用延迟初始化,无需关闭持久化的Redis连接 - @property - def protections(self): - """ - 保护机制配置 - 基于最新Freqtrade规范,保护机制应定义在策略文件中而非配置文件 - """ - return [ - { - "method": "StoplossGuard", - "lookback_period_candles": 60, # 3小时回看期(60根3分钟K线) - "trade_limit": 2, # 最多2笔止损交易 - "stop_duration_candles": 60, # 暂停180分钟(60根3分钟K线) - "only_per_pair": True # 仅针对单个币对 - }, - { - "method": "CooldownPeriod", - "stop_duration_candles": 2 # 6分钟冷却期(2根3分钟K线) - }, - { - "method": "MaxDrawdown", - "lookback_period_candles": 48, # 2.4小时回看期 - "trade_limit": 4, # 4笔交易限制 - "stop_duration_candles": 24, # 72分钟暂停(24根3分钟K线) - "max_allowed_drawdown": 0.20 # 20%最大回撤容忍度 - }, - { - "method": "TotalLossProtection", - # 10根半小时K线内亏损阈值(10美元) - "threshold_10_candles": 10.0, - # 20根半小时K线内亏损阈值(15美元) - "threshold_20_candles": 15.0, - # 锁定时长(2小时) - "stop_duration": 120 - }, - { - "method": "ConsecutiveLossProtection", - # 最大连续亏损次数 - "max_consecutive_losses": 3, - # 锁定时长(3小时) - "stop_duration": 180, - # 回看期(6小时) - "lookback_period": 360 - } - ] - def detect_trend_status(self, dataframe: DataFrame, metadata: dict) -> str: """ 基于分类模型优化 first_length 的趋势检测