diff --git a/freqtrade/templates/freqaiprimer.py b/freqtrade/templates/freqaiprimer.py index bdbd7474..62065efa 100644 --- a/freqtrade/templates/freqaiprimer.py +++ b/freqtrade/templates/freqaiprimer.py @@ -154,6 +154,11 @@ class FreqaiPrimer(IStrategy): return dataframe + @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 + + # 在 populate_indicators 中修改市场趋势相关逻辑 def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: pair = metadata.get('pair', 'Unknown') logger.info(f"[{pair}] 当前可用列(调用FreqAI前):{list(dataframe.columns)}") @@ -190,10 +195,10 @@ class FreqaiPrimer(IStrategy): # 添加调试日志 logger.debug(f"[{pair}] 最新数据 - close:{dataframe['close'].iloc[-1]:.6f}, " - f"rsi:{dataframe['rsi'].iloc[-1]:.2f}, " - 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"rsi:{dataframe['rsi'].iloc[-1]:.2f}, " + 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}") # 获取 labels_mean 和 labels_std labels_mean = None @@ -234,22 +239,10 @@ class FreqaiPrimer(IStrategy): labels_std = 0.01 logger.warning(f"[{pair}] labels_std 计算异常,使用默认值 0.01") - # 根据市场趋势动态调整买卖阈值 - market_trend = self.get_market_trend() - k_buy = 1.0 - k_sell = 1.2 - if market_trend == 'bull': - k_buy = 0.8 # 放宽买入阈值 - k_sell = 1.0 # 收紧卖出阈值 - elif market_trend == 'bear': - k_buy = 1.2 # 收紧买入阈值 - k_sell = 1.5 # 放宽卖出阈值 - else: - k_buy = 1.0 - k_sell = 1.2 - - if labels_mean > 0.015: - k_sell += 0.5 + # 根据市场趋势得分动态调整买卖阈值 + market_trend_score = self.get_market_trend() + k_buy = FreqaiPrimer.linear_map(market_trend_score, 0, 100, 1.2, 0.8) + k_sell = FreqaiPrimer.linear_map(market_trend_score, 0, 100, 1.5, 1.0) self.buy_threshold = labels_mean - k_buy * labels_std self.sell_threshold = labels_mean + k_sell * labels_std @@ -260,7 +253,7 @@ class FreqaiPrimer(IStrategy): self.sell_threshold = min(self.sell_threshold, self.SELL_THRESHOLD_MAX.value) self.sell_threshold = max(self.sell_threshold, self.SELL_THRESHOLD_MIN.value) - logger.info(f"[{pair}] 市场趋势:{market_trend}, labels_mean:{labels_mean:.4f}, labels_std:{labels_std:.4f}") + logger.info(f"[{pair}] 市场趋势得分:{market_trend_score}, labels_mean:{labels_mean:.4f}, labels_std:{labels_std:.4f}") logger.info(f"[{pair}] k_buy:{k_buy:.2f}, k_sell:{k_sell:.2f}") logger.info(f"[{pair}] 动态买入阈值:{self.buy_threshold:.4f}, 卖出阈值:{self.sell_threshold:.4f}") @@ -382,26 +375,10 @@ class FreqaiPrimer(IStrategy): **kwargs) -> float | None | tuple[float | None, str | None]: """ 动态调整仓位:支持加仓和分批减仓 - 参数: - trade: 当前交易对象 - current_time: 当前时间 - current_rate: 当前市场价格 - current_profit: 当前未实现利润百分比 - min_stake: 最小下单金额(可能为 None) - max_stake: 最大可用下单金额 - current_entry_rate: 当前入场价格 - current_exit_rate: 当前退出价格 - current_entry_profit: 当前入场利润百分比 - current_exit_profit: 当前退出利润百分比 - 返回值: - - 正数(float):加仓金额(报价货币,例如 USDT) - - 负数(float):减仓金额(例如 -30.0 表示卖出 30 USDT) - - None:不调整仓位 - - tuple[float | None, str | None]:(调整金额, 调整原因) """ pair = trade.pair hold_time = (current_time - trade.open_date_utc).total_seconds() / 60 - market_trend = self.get_market_trend() + market_trend_score = self.get_market_trend() profit_ratio = (current_rate - trade.open_rate) / trade.open_rate # --- 加仓逻辑 --- @@ -409,7 +386,7 @@ class FreqaiPrimer(IStrategy): if trade.nr_of_successful_entries <= max_entry_adjustments + 1: # +1 包括初始入场 add_position_threshold = self.ADD_POSITION_THRESHOLD.value if profit_ratio <= add_position_threshold and hold_time > 5: - if market_trend in ['bear', 'sideways']: # 熊市或震荡市更倾向加仓 + if market_trend_score <= 60: # 熊市或中性更倾向加仓 add_amount = 0.5 * trade.stake_amount # 加仓 50% 的初始仓位 if min_stake is not None and add_amount < min_stake: logger.warning(f"[{pair}] 加仓金额 {add_amount:.2f} 低于最小下单金额 {min_stake},取消加仓") @@ -420,16 +397,16 @@ class FreqaiPrimer(IStrategy): logger.info(f"[{pair}] 价格下跌 {profit_ratio*100:.2f}%,触发加仓 {add_amount:.2f}") return (add_amount, f"Price dropped {profit_ratio*100:.2f}%") else: - logger.debug(f"[{pair}] 价格下跌但市场趋势为 {market_trend},不加仓") + logger.debug(f"[{pair}] 价格下跌但市场趋势得分 {market_trend_score},不加仓") return None # --- 减仓逻辑 --- exit_position_ratio = self.EXIT_POSITION_RATIO.value if profit_ratio >= 0.03: # 利润达到 3% - if market_trend == 'bull': - reduce_amount = -exit_position_ratio * 0.6 * trade.stake_amount # 牛市减仓较少 - logger.info(f"[{pair}] 牛市,利润 {profit_ratio*100:.2f}%,减仓 {abs(reduce_amount):.2f}") - return (reduce_amount, f"Bull market, profit {profit_ratio*100:.2f}%") + if market_trend_score > 70: # 强牛市 + reduce_amount = -exit_position_ratio * 0.6 * trade.stake_amount # 减仓较少 + logger.info(f"[{pair}] 强牛市(得分 {market_trend_score}),利润 {profit_ratio*100:.2f}%,减仓 {abs(reduce_amount):.2f}") + return (reduce_amount, f"Strong bull market, profit {profit_ratio*100:.2f}%") else: reduce_amount = -exit_position_ratio * trade.stake_amount # 其他市场减仓较多 logger.info(f"[{pair}] 利润 {profit_ratio*100:.2f}%,减仓 {abs(reduce_amount):.2f}") @@ -442,16 +419,16 @@ class FreqaiPrimer(IStrategy): # --- 追踪止损逻辑 --- trailing_stop_start = self.TRAILING_STOP_START.value trailing_stop_distance = self.TRAILING_STOP_DISTANCE.value - if market_trend == 'bull': - trailing_stop_distance *= 1.5 # 牛市放宽追踪止损 + if market_trend_score > 70: # 强牛市 + trailing_stop_distance *= 1.5 trailing_stop_start *= 1.2 - elif market_trend == 'bear': - trailing_stop_distance *= 0.7 # 熊市收紧追踪止损 + elif market_trend_score < 30: # 强熊市 + trailing_stop_distance *= 0.7 trailing_stop_start *= 0.8 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) # 初始化最高和最低价格 + trade.adjust_min_max_rates(current_rate, current_rate) logger.info(f"[{pair}] 价格上涨超过 {trailing_stop_start*100:.1f}%,启动 Trailing Stop") return None @@ -461,7 +438,7 @@ class FreqaiPrimer(IStrategy): if current_rate < trailing_stop_price: logger.info(f"[{pair}] 价格回落至 Trailing Stop 点 {trailing_stop_price:.6f},触发全部卖出") return (-trade.stake_amount, f"Trailing stop at {trailing_stop_price:.6f}") - trade.adjust_min_max_rates(current_rate, trade.min_rate) # 更新最高价格 + trade.adjust_min_max_rates(current_rate, trade.min_rate) return None # --- 最大持仓时间限制 --- @@ -470,10 +447,20 @@ class FreqaiPrimer(IStrategy): return (-trade.stake_amount, "Max hold time exceeded") 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: - market_trend = self.get_market_trend() - cooldown_period_minutes = self.COOLDOWN_PERIOD_MINUTES.value if market_trend == 'bull' else self.COOLDOWN_PERIOD_MINUTES.value // 2 + # 调试日志:记录输入参数 + logger.debug(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}") + + # 检查 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.") + return False + + market_trend_score = self.get_market_trend() + cooldown_period_minutes = self.COOLDOWN_PERIOD_MINUTES.value if market_trend_score > 50 else self.COOLDOWN_PERIOD_MINUTES.value // 2 if pair in self.last_entry_time: last_time = self.last_entry_time[pair] @@ -483,7 +470,12 @@ class FreqaiPrimer(IStrategy): self.last_entry_time[pair] = current_time self.trailing_stop_enabled = False - logger.info(f"[{pair}] 确认入场,价格:{rate:.6f}") + try: + logger.info(f"[{pair}] 确认入场,价格:{float(rate):.6f}") + except (ValueError, TypeError) as e: + logger.error(f"[{pair}] Failed to format rate: {rate} (type: {type(rate)}), error: {e}") + return False + return True def confirm_trade_exit(self, pair: str, trade: Trade, order_type: str, amount: float, @@ -507,28 +499,118 @@ class FreqaiPrimer(IStrategy): logger.debug(f"[{pair}] 自定义卖出价:{adjusted_rate:.6f}(原价:{proposed_rate:.6f})") return adjusted_rate - def get_market_trend(self, dataframe: DataFrame = None) -> str: + def get_market_trend(self, dataframe: DataFrame = None) -> int: + """ + 计算市场趋势得分,0 表示强熊,100 表示强牛,50 表示中性。 + 使用对数函数映射,非线性增强两端趋势。 + 综合价格趋势、K 线形态、StochRSI 和量价关系。 + + 参数: + dataframe: 可选,外部传入的 BTC/USDT 数据,默认为 None(自动获取)。 + + 返回值: + int: 0-100 的趋势得分。 + """ try: - btc_df = self.dp.get_pair_dataframe("BTC/USDT", self.timeframe) + # 获取 BTC/USDT 数据 + btc_df = dataframe if dataframe is not None else self.dp.get_pair_dataframe("BTC/USDT", self.timeframe) if len(btc_df) < 200: - logger.warning("BTC 数据不足,返回默认趋势:sideways") - return "sideways" + logger.warning("BTC 数据不足,返回默认趋势得分:50") + return 50 + # --- 价格趋势 --- + btc_df["ema50"] = ta.EMA(btc_df, timeperiod=50) btc_df["ema200"] = ta.EMA(btc_df, timeperiod=200) - btc_df["adx"] = ta.ADX(btc_df, timeperiod=14) - trend_strength = btc_df["close"].pct_change(50).iloc[-1] - + btc_df["ema50_slope"] = (btc_df["ema50"] - btc_df["ema50"].shift(10)) / btc_df["ema50"].shift(10) + price_above_ema = btc_df["close"].iloc[-1] > btc_df["ema200"].iloc[-1] - adx_value = btc_df["adx"].iloc[-1] + ema50_above_ema200 = btc_df["ema50"].iloc[-1] > btc_df["ema200"].iloc[-1] + ema50_slope = btc_df["ema50_slope"].iloc[-1] + + price_score = 0 + if price_above_ema: + price_score += 20 + if ema50_above_ema200: + price_score += 20 + if ema50_slope > 0.005: + price_score += 15 + elif ema50_slope < -0.005: + price_score -= 15 - if price_above_ema and trend_strength > 0.03 and adx_value > 25: - return "bull" - elif not price_above_ema and trend_strength < -0.03 and adx_value > 25: - return "bear" + # --- K 线形态 --- + btc_df["bullish_engulfing"] = ( + (btc_df["close"].shift(1) < btc_df["open"].shift(1)) & # 前一天阴线 + (btc_df["close"] > btc_df["open"]) & # 当天阳线 + (btc_df["close"] > btc_df["open"].shift(1)) & # 吞没前一天开盘价 + (btc_df["open"] < btc_df["close"].shift(1)) # 吞没前一天收盘价 + ) + btc_df["bearish_engulfing"] = ( + (btc_df["close"].shift(1) > btc_df["open"].shift(1)) & # 前一天阳线 + (btc_df["close"] < btc_df["open"]) & # 当天阴线 + (btc_df["close"] < btc_df["open"].shift(1)) & # 吞没前一天开盘价 + (btc_df["open"] > btc_df["close"].shift(1)) # 吞没前一天收盘价 + ) + + kline_score = 0 + if btc_df["bullish_engulfing"].iloc[-1]: + kline_score += 15 + elif btc_df["bearish_engulfing"].iloc[-1]: + kline_score -= 15 + # 近期波动性 + volatility = btc_df["close"].pct_change(10).std() * 100 + if volatility > 0.5: + kline_score += 10 if price_score > 0 else -10 + + # --- StochRSI --- + stochrsi = ta.STOCHRSI(btc_df, timeperiod=14, fastk_period=3, fastd_period=3) + btc_df["stochrsi_k"] = stochrsi["fastk"] + btc_df["stochrsi_d"] = stochrsi["fastd"] + + stochrsi_score = 0 + stochrsi_k = btc_df["stochrsi_k"].iloc[-1] + stochrsi_d = btc_df["stochrsi_d"].iloc[-1] + if stochrsi_k > 80 and stochrsi_k < stochrsi_d: + stochrsi_score -= 15 # 超买且 K 下穿 D + elif stochrsi_k < 20 and stochrsi_k > stochrsi_d: + stochrsi_score += 15 # 超卖且 K 上穿 D + elif stochrsi_k > 50: + stochrsi_score += 5 + elif stochrsi_k < 50: + stochrsi_score -= 5 + + # --- 量价关系 --- + btc_df["volume_mean_20"] = btc_df["volume"].rolling(20).mean() + btc_df["volume_std_20"] = btc_df["volume"].rolling(20).std() + btc_df["volume_z_score"] = (btc_df["volume"] - btc_df["volume_mean_20"]) / btc_df["volume_std_20"] + btc_df["adx"] = ta.ADX(btc_df, timeperiod=14) + + volume_score = 0 + if btc_df["volume_z_score"].iloc[-1] > 1.5: + volume_score += 10 if price_score > 0 else -10 + if btc_df["adx"].iloc[-1] > 25: + volume_score += 10 if price_score > 0 else -10 + + # --- 综合得分 --- + raw_score = price_score + kline_score + stochrsi_score + volume_score + # 归一化到 [-50, 50] + raw_score = max(min(raw_score, 50), -50) + + # 对数映射到 [0, 100],50 为中性 + if raw_score >= 0: + # 正向:50 到 100,log(x + 1) 确保非线性 + mapped_score = 50 + 50 * (np.log1p(raw_score / 50) / np.log1p(1)) else: - return "sideways" + # 负向:0 到 50,log(1 - x) + mapped_score = 50 * (np.log1p(-raw_score / 50) / np.log1p(1)) + + final_score = int(round(mapped_score)) + final_score = max(0, min(100, final_score)) + + logger.debug(f"BTC 趋势得分:{final_score}, 原始得分:{raw_score}, " + f"价格得分:{price_score}, K线得分:{kline_score}, " + f"StochRSI得分:{stochrsi_score}, 量价得分:{volume_score}") + return final_score + except Exception as e: logger.error(f"获取市场趋势失败:{e}", exc_info=True) - return "sideways" - - + return 50