From 4175d676387c862d27ef62e76e7e57847e2369a7 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Mon, 23 Jun 2025 18:39:15 +0800 Subject: [PATCH] Revert "add logs" This reverts commit db95f4bb588a2b234535e2a8a42fbc64148e8a98. --- freqtrade/templates/freqaiprimer.json | 34 +-- freqtrade/templates/freqaiprimer.py | 390 ++++++++++++-------------- 2 files changed, 196 insertions(+), 228 deletions(-) diff --git a/freqtrade/templates/freqaiprimer.json b/freqtrade/templates/freqaiprimer.json index 22d689a8..95324db3 100644 --- a/freqtrade/templates/freqaiprimer.json +++ b/freqtrade/templates/freqaiprimer.json @@ -8,33 +8,33 @@ "max_open_trades": 5 }, "buy": { - "ADD_POSITION_THRESHOLD": -0.01, - "BUY_THRESHOLD_MAX": -0.017, - "BUY_THRESHOLD_MIN": -0.044, - "COOLDOWN_PERIOD_MINUTES": 10, + "ADD_POSITION_THRESHOLD": -0.028, + "BUY_THRESHOLD_MAX": -0.003, + "BUY_THRESHOLD_MIN": -0.028, + "COOLDOWN_PERIOD_MINUTES": 4, "MAX_ENTRY_POSITION_ADJUSTMENT": 2 }, "sell": { - "EXIT_POSITION_RATIO": 0.377, - "SELL_THRESHOLD_MAX": 0.049, - "SELL_THRESHOLD_MIN": 0.002, - "TRAILING_STOP_DISTANCE": 0.018, - "TRAILING_STOP_START": 0.044 + "EXIT_POSITION_RATIO": 0.335, + "SELL_THRESHOLD_MAX": 0.054, + "SELL_THRESHOLD_MIN": 0.001, + "TRAILING_STOP_DISTANCE": 0.005, + "TRAILING_STOP_START": 0.048 }, "protection": {}, "roi": { - "0": 0.20099999999999998, - "9": 0.051, - "16": 0.007, - "69": 0 + "0": 0.156, + "20": 0.057999999999999996, + "50": 0.018, + "113": 0 }, "trailing": { "trailing_stop": true, - "trailing_stop_positive": 0.106, - "trailing_stop_positive_offset": 0.126, - "trailing_only_offset_is_reached": true + "trailing_stop_positive": 0.172, + "trailing_stop_positive_offset": 0.23199999999999998, + "trailing_only_offset_is_reached": false } }, "ft_stratparam_v": 1, - "export_time": "2025-06-23 06:56:55.803622+00:00" + "export_time": "2025-06-23 00:37:56.498155+00:00" } \ No newline at end of file diff --git a/freqtrade/templates/freqaiprimer.py b/freqtrade/templates/freqaiprimer.py index 463f6930..a51552e6 100644 --- a/freqtrade/templates/freqaiprimer.py +++ b/freqtrade/templates/freqaiprimer.py @@ -108,14 +108,6 @@ class FreqaiPrimer(IStrategy): self.stats_logged = False self.fit_live_predictions_candles = self.freqai_info.get("fit_live_predictions_candles", 100) self.last_entry_time = {} # 记录每个币种的最后入场时间 - self.indicator_cache = {} # 缓存技术指标用于日志记录 - - # 设置交易专用日志 - trade_log_handler = logging.FileHandler("trade_log.jsonl") - trade_log_handler.setFormatter(logging.Formatter("%(asctime)s - %(message)s")) - self.trade_logger = logging.getLogger("trade_logger") - self.trade_logger.setLevel(logging.INFO) - self.trade_logger.addHandler(trade_log_handler) def feature_engineering_expand_all(self, dataframe: DataFrame, period: int, metadata: dict, **kwargs) -> DataFrame: dataframe["%-rsi-period"] = ta.RSI(dataframe, timeperiod=period) @@ -166,6 +158,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)}") @@ -202,10 +199,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 @@ -260,7 +257,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}") @@ -326,13 +323,6 @@ class FreqaiPrimer(IStrategy): combined_condition = reduce(lambda x, y: x & y, conditions) if combined_condition.any(): dataframe.loc[combined_condition, 'enter_long'] = 1 - # 缓存指标用于交易日志 - self.indicator_cache[pair] = { - "rsi": dataframe["rsi"].iloc[-1], - "volume_z_score": dataframe["volume_z_score"].iloc[-1], - "&-price_value_divergence": dataframe["&-price_value_divergence"].iloc[-1], - "bb_lowerband": dataframe["bb_lowerband"].iloc[-1] - } logger.debug(f"[{pair}] 入场信号触发,条件满足") else: logger.debug(f"[{pair}] 买入条件均不满足,未触发入场信号") @@ -358,19 +348,8 @@ class FreqaiPrimer(IStrategy): logger.warning(f"[{pair}] ⚠️ &-price_value_divergence 列缺失,跳过该条件") if len(conditions) > 0: - combined_condition = reduce(lambda x, y: x & y, conditions) - if combined_condition.any(): - dataframe.loc[combined_condition, 'exit_long'] = 1 - # 缓存指标用于交易日志 - self.indicator_cache[pair] = { - "rsi": dataframe["rsi"].iloc[-1], - "volume_z_score": dataframe["volume_z_score"].iloc[-1], - "&-price_value_divergence": dataframe["&-price_value_divergence"].iloc[-1], - "bb_upperband": dataframe["bb_upperband"].iloc[-1] - } - logger.debug(f"[{pair}] 出场信号触发,条件满足") - else: - logger.debug(f"[{pair}] 卖出条件均不满足,未触发出场信号") + dataframe.loc[reduce(lambda x, y: x & y, conditions), 'exit_long'] = 1 + logger.debug(f"[{pair}] 出场信号触发,条件满足") else: logger.debug(f"[{pair}] 无有效卖出条件") @@ -399,238 +378,226 @@ 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: 最小下单金额(可能为 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]:(调整金额, 调整原因) + - 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 - market_trend = self.get_market_trend() profit_ratio = (current_rate - trade.open_rate) / trade.open_rate - # --- 加仓逻辑 --- - max_entry_adjustments = self.MAX_ENTRY_POSITION_ADJUSTMENT.value - 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']: # 熊市或震荡市更倾向加仓 - 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},取消加仓") - return (None, "Add amount below min_stake") - if add_amount > max_stake: - logger.warning(f"[{pair}] 加仓金额 {add_amount:.2f} 超出最大可用金额 {max_stake},取消加仓") - return (None, "Add amount exceeds max_stake") - 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},不加仓") - return None + initial_stake_amount = trade.stake_amount / 3 + logger.debug(f"{pair} 首次入场金额: {initial_stake_amount:.2f}, 当前持仓金额: {trade.stake_amount:.2f}, " + f"加仓次数: {trade.nr_of_successful_entries - 1}") - # --- 减仓逻辑 --- + # 动态最大持仓时间 + # max_hold_time = self.MAX_HOLD_TIME_MINUTES.value + # # 线性映射持仓时间因子 + # hold_factor = self.HOLD_TIME_REDUCTION.value + (self.HOLD_TIME_EXTENSION.value - self.HOLD_TIME_REDUCTION.value) * (trend_score / 100) + # max_hold_time *= hold_factor + # if profit_ratio > 0.02: + # max_hold_time *= 1.2 + # elif profit_ratio < -0.02: + # max_hold_time *= 0.8 + # max_hold_time = int(max_hold_time) + + # 加仓逻辑 + max_entry_adjustments = self.MAX_ENTRY_POSITION_ADJUSTMENT.value + if trade.nr_of_successful_entries <= max_entry_adjustments + 1: + add_position_threshold = self.ADD_POSITION_THRESHOLD.value + # 线性映射加仓阈值,趋势值越高,加仓越严格 + add_threshold = 80 - 30 * (trend_score / 100) # 趋势值 100 -> 50, 0 -> 80 + if profit_ratio <= add_position_threshold and hold_time > 5 and trend_score <= add_threshold: + logger.debug(f"{pair} 初始下注金额: {initial_stake_amount:.2f}, trend_score: {trend_score:.2f}, add_threshold: {add_threshold} ") + + # 计算加仓金额 + add_count = trade.nr_of_successful_entries - 1 + multipliers = [2, 4, 8] + if add_count < len(multipliers): + multiplier = multipliers[add_count] + add_amount = initial_stake_amount * multiplier + logger.debug(f"{pair} 第 {add_count + 1} 次加仓,倍数={multiplier}, " + f"金额 = {initial_stake_amount:.2f} * {multiplier} = {add_amount:.2f}") + logger.debug(f"{pair} 加仓计算: 第 {add_count + 1} 次加仓,倍数={multiplier}, " + f"金额 = {initial_stake_amount:.2f} * {multiplier} = {add_amount:.2f}") + + if min_stake is not None and add_amount < min_stake: + logger.warning(f"{pair} 加仓金额 {add_amount:.2f} 低于最小下注金额 {min_stake:.2f},取消加仓") + return (None, f"Add amount {add_amount:.2f} below min_stake {min_stake:.2f}") + if add_amount > max_stake: + logger.warning(f"{pair} 加仓金额 {add_amount:.2f} 超出最大可用金额 {max_stake:.2f},调整为 {max_stake:.2f}") + add_amount = max_stake + logger.info(f"{pair} 价格下跌 {profit_ratio*100:.2f}%,触发第 {add_count + 1} 次加仓 {add_amount:.2f}") + return (add_amount, f"Price dropped {profit_ratio*100:.2f}%, add {add_amount:.2f}") + + # 减仓逻辑 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}%") - else: - reduce_amount = -exit_position_ratio * trade.stake_amount # 其他市场减仓较多 - logger.info(f"[{pair}] 利润 {profit_ratio*100:.2f}%,减仓 {abs(reduce_amount):.2f}") - return (reduce_amount, f"Profit {profit_ratio*100:.2f}%") - elif profit_ratio >= 0.05: # 利润达到 5% - reduce_amount = -exit_position_ratio * 1.4 * trade.stake_amount # 卖出更多 - logger.info(f"[{pair}] 利润 {profit_ratio*100:.2f}%,减仓 {abs(reduce_amount):.2f}") + if profit_ratio >= 0.03: + # 趋势值越高,减仓比例越低 + reduce_factor = 0.6 + 0.4 * (1 - trend_score / 100) # 牛市(100) -> 0.6, 熊市(0) -> 1.0 + reduce_amount = -exit_position_ratio * reduce_factor * trade.stake_amount + logger.info(f"{pair} 趋势值 {trend_score:.2f},利润 {profit_ratio*100:.2f}%,减仓 {abs(reduce_amount):.2f}") + return (reduce_amount, f"Profit {profit_ratio*100:.2f}%") + elif profit_ratio >= 0.05: + reduce_factor = 1.4 - 0.4 * (trend_score / 100) # 牛市(100) -> 1.0, 熊市(0) -> 1.4 + reduce_amount = -exit_position_ratio * reduce_factor * trade.stake_amount + logger.info(f"{pair} 趋势值 {trend_score:.2f},利润 {profit_ratio*100:.2f}%,减仓 {abs(reduce_amount):.2f}") return (reduce_amount, f"Profit {profit_ratio*100:.2f}%") - # --- 追踪止损逻辑 --- + # 追踪止损逻辑 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 # 牛市放宽追踪止损 - trailing_stop_start *= 1.2 - elif market_trend == 'bear': - trailing_stop_distance *= 0.7 # 熊市收紧追踪止损 - trailing_stop_start *= 0.8 + # 使用 Sigmoid 映射调整追踪止损参数 + sigmoid = 1 / (1 + np.exp(-0.1 * (trend_score - 50))) + trailing_factor = 0.8 + (1.2 - 0.8) * sigmoid # 牛市(100) -> 1.2, 熊市(0) -> 0.8 + distance_factor = 0.7 + (1.5 - 0.7) * sigmoid # 牛市(100) -> 1.5, 熊市(0) -> 0.7 + trailing_stop_start *= trailing_factor + trailing_stop_distance *= distance_factor 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}] 价格上涨超过 {trailing_stop_start*100:.1f}%,启动 Trailing Stop") + trade.adjust_min_max_rates(current_rate, current_rate) + logger.info(f"{pair} 价格上涨超过 {trailing_stop_start*100:.1f}%,启动追踪止损") return None if self.trailing_stop_enabled: - max_rate = trade.max_rate + 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 点 {trailing_stop_price:.6f},触发全部卖出") + logger.info(f"{pair} 价格回落至 {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 - # --- 最大持仓时间限制 --- - if hold_time > 30: - logger.info(f"[{pair}] 持仓时间超过30分钟,强制清仓") - return (-trade.stake_amount, "Max hold time exceeded") + # 最大持仓时间限制 + # if hold_time > max_hold_time: + # logger.info(f"{pair} 趋势值 {trend_score:.2f},持仓时间超过 {max_hold_time} 分钟,强制清仓") + # return (-trade.stake_amount, f"Max hold time ({max_hold_time} min) 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] if (current_time - last_time).total_seconds() < cooldown_period_minutes * 60: logger.info(f"[{pair}] 冷却期内({cooldown_period_minutes} 分钟),跳过本次入场") return False - # 记录进入交易的详细信息 - entry_log = { - "pair": pair, - "order_type": order_type, - "amount": amount, - "rate": rate, - "total_value": amount * rate, - "time_in_force": time_in_force, - "timestamp": current_time.isoformat(), - "market_trend": market_trend, - "cooldown_period_minutes": cooldown_period_minutes, - "buy_threshold": self.buy_threshold, - "indicators": self.indicator_cache.get(pair, { - "rsi": "N/A", - "volume_z_score": "N/A", - "&-price_value_divergence": "N/A", - "bb_lowerband": "N/A" - }) - } - logger.info(f"[{pair}] 交易进入确认: {json.dumps(entry_log, indent=2)}") - self.trade_logger.info(json.dumps(entry_log)) - - # 更新最后入场时间并重置追踪止损 self.last_entry_time[pair] = current_time self.trailing_stop_enabled = False + 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, - rate: float, time_in_force: str, exit_reason: str, - current_time: datetime, **kwargs) -> bool: - """ - 确认交易退出时的逻辑,并记录详细日志 - """ + rate: float, time_in_force: str, exit_reason: str, + current_time: datetime, **kwargs) -> bool: adjusted_rate = rate * (1 + 0.0025) - profit_ratio = trade.calc_profit_ratio(rate) - profit_amount = trade.calc_profit(rate) - hold_time_minutes = (current_time - trade.open_date_utc).total_seconds() / 60 - - # 记录退出交易的详细信息 - exit_log = { - "pair": pair, - "order_type": order_type, - "amount": amount, - "rate": rate, - "adjusted_rate": adjusted_rate, - "total_value": amount * rate, - "time_in_force": time_in_force, - "timestamp": current_time.isoformat(), - "exit_reason": exit_reason, - "profit_ratio": profit_ratio, - "profit_amount": profit_amount, - "hold_time_minutes": hold_time_minutes, - "market_trend": self.get_market_trend(), - "sell_threshold": self.sell_threshold, - "indicators": self.indicator_cache.get(pair, { - "rsi": "N/A", - "volume_z_score": "N/A", - "&-price_value_divergence": "N/A", - "bb_upperband": "N/A" - }), - "trade_details": { - "open_rate": trade.open_rate, - "open_date": trade.open_date_utc.isoformat(), - "stake_amount": trade.stake_amount, - "nr_of_successful_entries": trade.nr_of_successful_entries - } - } - logger.info(f"[{pair}] 交易退出确认: {json.dumps(exit_log, indent=2)}") - self.trade_logger.info(json.dumps(exit_log)) - + logger.info(f"[{pair}] 退出交易,原因:{exit_reason}, 原始利润:{trade.calc_profit_ratio(rate):.2%}," + f"调整后卖出价:{adjusted_rate:.6f}") return True def custom_entry_price(self, pair: str, trade: Trade | None, current_time: datetime, proposed_rate: float, - entry_tag: str | None, side: str, **kwargs) -> float: + entry_tag: str | None, side: str, **kwargs) -> float: adjusted_rate = proposed_rate * (1 - 0.005) logger.debug(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: + current_time: datetime, proposed_rate: float, + current_profit: float, exit_tag: str | None, **kwargs) -> float: adjusted_rate = proposed_rate * (1 + 0.0025) logger.debug(f"[{pair}] 自定义卖出价:{adjusted_rate:.6f}(原价:{proposed_rate:.6f})") return adjusted_rate - def get_market_trend(self, dataframe: DataFrame = None, metadata: dict = None) -> str: + def get_market_trend(self, dataframe: DataFrame = None, metadata: dict = None) -> int: """ - 判断市场趋势:bull(牛市)、bear(熊市)、sideways(震荡市) - 基于多时间框架的EMA、K线形态和量价关系 + 计算市场趋势得分,0 表示强熊,100 表示强牛,50 表示中性。 + 综合 3m、15m 和 1h 时间框架的趋势,使用对数函数映射,非线性增强两端趋势。 + 融合价格趋势、K 线形态、StochRSI 和量价关系。 + + 参数: + dataframe: 可选,外部传入的 BTC/USDT 数据(3m 时间框架),默认为 None(自动获取)。 + metadata: 可选,包含币种信息。 + + 返回值: + int: 0-100 的趋势得分。 """ try: + # 初始化得分和权重 timeframes = ["3m", "15m", "1h"] - weights = {"3m": 0.5, "15m": 0.3, "1h": 0.2} + weights = {"3m": 0.5, "15m": 0.3, "1h": 0.2} # 短期框架权重更高 trend_scores = {} pair = metadata.get('pair', 'Unknown') if metadata else 'Unknown' - logger.debug(f"[{pair}] 正在计算多时间框架市场趋势") + logger.debug(f"[{pair}] 正在计算多时间框架市场趋势得分") for tf in timeframes: + # 获取对应时间框架的数据 if tf == "3m" and dataframe is not None: btc_df = dataframe else: btc_df = self.dp.get_pair_dataframe("BTC/USDT", tf) - min_candles = 200 if tf == "3m" else 100 if tf == "15m" else 50 + # 检查数据量 + min_candles = 200 if tf == "3m" else 100 if tf == "15m" else 50 # 不同时间框架所需最小数据量 if len(btc_df) < min_candles: - logger.warning(f"[{pair}] BTC 数据不足({tf},{len(btc_df)} 根K线),默认震荡市") + logger.warning(f"BTC 数据不足({tf},{len(btc_df)} 根K线),使用默认得分:50") trend_scores[tf] = 50 continue - # 计算EMA - btc_df["ema_short"] = ta.EMA(btc_df, timeperiod=50 if tf == "3m" else 20 if tf == "15m" else 12) - btc_df["ema_long"] = ta.EMA(btc_df, timeperiod=200 if tf == "3m" else 80 if tf == "15m" else 50) + # --- 价格趋势 --- + ema_short_period = 50 if tf == "3m" else 20 if tf == "15m" else 12 # 调整周期以适应时间框架 + ema_long_period = 200 if tf == "3m" else 80 if tf == "15m" else 50 + btc_df["ema_short"] = ta.EMA(btc_df, timeperiod=ema_short_period) + btc_df["ema_long"] = ta.EMA(btc_df, timeperiod=ema_long_period) btc_df["ema_short_slope"] = (btc_df["ema_short"] - btc_df["ema_short"].shift(10)) / btc_df["ema_short"].shift(10) - # 价格趋势得分 + price_above_ema = btc_df["close"].iloc[-1] > btc_df["ema_long"].iloc[-1] + ema_short_above_ema_long = btc_df["ema_short"].iloc[-1] > btc_df["ema_long"].iloc[-1] + ema_short_slope = btc_df["ema_short_slope"].iloc[-1] + price_score = 0 - if btc_df["close"].iloc[-1] > btc_df["ema_long"].iloc[-1]: + if price_above_ema: price_score += 20 - if btc_df["ema_short"].iloc[-1] > btc_df["ema_long"].iloc[-1]: + if ema_short_above_ema_long: price_score += 20 - if btc_df["ema_short_slope"].iloc[-1] > 0.005: + if ema_short_slope > 0.005: price_score += 15 - elif btc_df["ema_short_slope"].iloc[-1] < -0.005: + elif ema_short_slope < -0.005: price_score -= 15 - # K线形态得分 + # --- K 线形态 --- btc_df["bullish_engulfing"] = ( (btc_df["close"].shift(1) < btc_df["open"].shift(1)) & (btc_df["close"] > btc_df["open"]) & @@ -643,6 +610,7 @@ class FreqaiPrimer(IStrategy): (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 @@ -652,60 +620,60 @@ class FreqaiPrimer(IStrategy): if volatility > 0.5: kline_score += 10 if price_score > 0 else -10 - # StochRSI得分 + # --- 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 - if btc_df["stochrsi_k"].iloc[-1] > 80 and btc_df["stochrsi_k"].iloc[-1] < btc_df["stochrsi_d"].iloc[-1]: + 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 - elif btc_df["stochrsi_k"].iloc[-1] < 20 and btc_df["stochrsi_k"].iloc[-1] > btc_df["stochrsi_d"].iloc[-1]: + elif stochrsi_k < 20 and stochrsi_k > stochrsi_d: stochrsi_score += 15 - elif btc_df["stochrsi_k"].iloc[-1] > 50: + elif stochrsi_k > 50: stochrsi_score += 5 - elif btc_df["stochrsi_k"].iloc[-1] < 50: + 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 raw_score = max(min(raw_score, 50), -50) + + # 对数映射到 [0, 100] if raw_score >= 0: mapped_score = 50 + 50 * (np.log1p(raw_score / 50) / np.log1p(1)) else: mapped_score = 50 * (np.log1p(-raw_score / 50) / np.log1p(1)) + trend_scores[tf] = max(0, min(100, int(round(mapped_score)))) - logger.debug(f"[{pair}] {tf} 趋势得分:{trend_scores[tf]}, " - f"原始得分:{raw_score}, 价格得分:{price_score}, " - f"K线得分:{kline_score}, StochRSI得分:{stochrsi_score}, " - f"量价得分:{volume_score}") + logger.debug(f"[{pair}] {tf} 趋势得分:{trend_scores[tf]}, 原始得分:{raw_score}, " + f"价格得分:{price_score}, K线得分:{kline_score}, " + f"StochRSI得分:{stochrsi_score}, 量价得分:{volume_score}") - # 加权融合 + # 加权融合多时间框架得分 final_score = sum(trend_scores[tf] * weights[tf] for tf in timeframes) - final_score = max(0, min(100, int(round(final_score)))) - logger.info(f"[{pair}] 最终趋势得分:{final_score}, " - f"3m得分:{trend_scores.get('3m', 50)}, " - f"15m得分:{trend_scores.get('15m', 50)}, " - f"1h得分:{trend_scores.get('1h', 50)}") + final_score = int(round(final_score)) + final_score = max(0, min(100, final_score)) - # 映射到趋势类别 - if final_score >= 70: - return 'bull' - elif final_score <= 30: - return 'bear' - else: - return 'sideways' + logger.info(f"[{pair}] 最终趋势得分:{final_score}, " + f"3m得分:{trend_scores.get('3m', 50)}, 15m得分:{trend_scores.get('15m', 50)}, " + f"1h得分:{trend_scores.get('1h', 50)}") + return final_score except Exception as e: logger.error(f"[{pair}] 获取市场趋势失败:{e}", exc_info=True) - return 'sideways' + return 50