import logging import numpy as np import datetime import os import json import glob from functools import reduce from freqtrade.persistence import Trade import talib.abstract as ta from pandas import DataFrame from typing import Dict from freqtrade.strategy import (DecimalParameter, IStrategy, IntParameter) logger = logging.getLogger(__name__) class FreqaiPrimer(IStrategy): """ 基于 FreqAI 的动态阈值交易策略,集成动态加仓和减仓逻辑,兼容最新 Freqtrade 版本 """ # --- 🧪 Hyperopt Parameters --- ROI_T0 = DecimalParameter(0.01, 0.05, default=0.02, space='roi', optimize=True) ROI_T1 = DecimalParameter(0.005, 0.02, default=0.01, space='roi', optimize=True) ROI_T2 = DecimalParameter(0.0, 0.01, default=0.0, space='roi', optimize=True) TRAILING_STOP_START = DecimalParameter(0.01, 0.05, default=0.03, space='sell', optimize=True) TRAILING_STOP_DISTANCE = DecimalParameter(0.005, 0.02, default=0.01, space='sell', optimize=True) BUY_THRESHOLD_MIN = DecimalParameter(-0.1, -0.01, default=-0.05, space='buy', optimize=True) BUY_THRESHOLD_MAX = DecimalParameter(-0.02, -0.001, default=-0.005, space='buy', optimize=True) SELL_THRESHOLD_MIN = DecimalParameter(0.001, 0.02, default=0.005, space='sell', optimize=True) SELL_THRESHOLD_MAX = DecimalParameter(0.02, 0.1, default=0.05, space='sell', optimize=True) # 新增:加仓和减仓参数 ADD_POSITION_THRESHOLD = DecimalParameter(-0.05, -0.01, default=-0.02, space='buy', optimize=True) EXIT_POSITION_RATIO = DecimalParameter(0.2, 0.7, default=0.5, space='sell', optimize=True) COOLDOWN_PERIOD_MINUTES = IntParameter(1, 10, default=5, space='buy', optimize=True) MAX_ENTRY_POSITION_ADJUSTMENT = IntParameter(1, 3, default=2, space='buy', optimize=True) # --- 🛠️ 固定配置参数 --- minimal_roi = { "0": ROI_T0.value, "30": ROI_T1.value, "60": ROI_T2.value } stoploss = -0.015 timeframe = "3m" use_custom_stoploss = True position_adjustment_enable = True # 启用动态仓位调整 plot_config = { "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"} }, "Price-Value Divergence": { "&-price_value_divergence": {"color": "purple"} }, "Volume Z-Score": { "volume_z_score": {"color": "orange"} }, "RSI": { "rsi": {"color": "cyan"} } } } freqai_info = { "identifier": "test58", "model": "LightGBMRegressor", "feature_parameters": { "include_timeframes": ["3m", "15m", "1h"], "label_period_candles": 12, "include_shifted_candles": 3, }, "data_split_parameters": { "test_size": 0.2, "shuffle": False, }, "model_training_parameters": { "n_estimators": 200, "learning_rate": 0.05, "num_leaves": 31, "verbose": -1, }, "fit_live_predictions_candles": 100, "live_retrain_candles": 100, } def __init__(self, config: dict, *args, **kwargs): super().__init__(config, *args, **kwargs) logger.setLevel(logging.DEBUG) logger.debug("✅ 策略已初始化,日志级别设置为 DEBUG") self.trailing_stop_enabled = False self.pair_stats = {} 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) dataframe["%-sma-period"] = ta.SMA(dataframe, timeperiod=period) dataframe["%-ema-period"] = ta.EMA(dataframe, timeperiod=period) real = ta.TYPPRICE(dataframe) upperband, middleband, lowerband = ta.BBANDS(real, timeperiod=period, nbdevup=2.0, nbdevdn=2.0) 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_lowerband-period"]) / dataframe["bb_middleband-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["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", "%-relative_volume-period", "%-price_value_divergence" ] for col in columns_to_clean: dataframe[col] = dataframe[col].replace([np.inf, -np.inf], 0).ffill().fillna(0) pair = metadata.get('pair', 'Unknown') logger.debug(f"[{pair}] 特征工程完成,列:{list(dataframe.columns)}") return dataframe def set_freqai_targets(self, dataframe: DataFrame, metadata: dict, **kwargs) -> DataFrame: pair = metadata.get('pair', 'Unknown') if len(dataframe) < 200: logger.warning(f"[{pair}] 数据量不足({len(dataframe)}根K线),需要至少200根K线进行训练") return dataframe dataframe["ema200"] = ta.EMA(dataframe, timeperiod=200) dataframe["&-price_value_divergence"] = (dataframe["close"] - dataframe["ema200"]) / dataframe["ema200"] 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"] 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) return dataframe def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: pair = metadata.get('pair', 'Unknown') logger.info(f"[{pair}] 当前可用列(调用FreqAI前):{list(dataframe.columns)}") # 计算200周期EMA和历史价值背离 dataframe["ema200"] = ta.EMA(dataframe, timeperiod=200) dataframe["price_value_divergence"] = (dataframe["close"] - dataframe["ema200"]) / dataframe["ema200"] # 调用FreqAI预测价值背离 if not hasattr(self, 'freqai') or self.freqai is None: logger.error(f"[{pair}] FreqAI 未初始化,请确保回测命令中启用了 --freqai") dataframe["&-price_value_divergence"] = dataframe["price_value_divergence"] else: logger.debug(f"self.freqai 类型:{type(self.freqai)}") dataframe = self.freqai.start(dataframe, metadata, self) if "&-price_value_divergence" not in dataframe.columns: logger.warning(f"[{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.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}") # 获取 labels_mean 和 labels_std labels_mean = None labels_std = None try: model_base_dir = os.path.join(self.config["user_data_dir"], "models", self.freqai_info["identifier"]) pair_base = pair.split('/')[0] if '/' in pair else pair sub_dirs = glob.glob(os.path.join(model_base_dir, f"sub-train-{pair_base}_*")) if not sub_dirs: logger.warning(f"[{pair}] 未找到任何子目录:{model_base_dir}/sub-train-{pair_base}_*") else: latest_sub_dir = max(sub_dirs, key=lambda x: int(x.split('_')[-1])) pair_base_lower = pair_base.lower() timestamp = latest_sub_dir.split('_')[-1] metadata_file = os.path.join(latest_sub_dir, f"cb_{pair_base_lower}_{timestamp}_metadata.json") if os.path.exists(metadata_file): with open(metadata_file, "r") as f: metadata = json.load(f) labels_mean = metadata["labels_mean"]["&-price_value_divergence"] labels_std = metadata["labels_std"]["&-price_value_divergence"] logger.info(f"[{pair}] 从最新子目录 {latest_sub_dir} 读取 labels_mean:{labels_mean}, labels_std:{labels_std}") else: logger.warning(f"[{pair}] 最新的 metadata.json 文件 {metadata_file} 不存在") except Exception as e: logger.warning(f"[{pair}] 无法从子目录读取 labels_mean 和 labels_std:{e},重新计算") if labels_mean is None or labels_std is None: logger.warning(f"[{pair}] 无法获取 labels_mean 和 labels_std,重新计算") dataframe["&-price_value_divergence_actual"] = (dataframe["close"] - dataframe["ema200"]) / dataframe["ema200"] dataframe["&-price_value_divergence_actual"] = dataframe["&-price_value_divergence_actual"].replace([np.inf, -np.inf], 0).ffill().fillna(0) recent_data = dataframe["&-price_value_divergence_actual"].tail(self.fit_live_predictions_candles) labels_mean = recent_data.mean() labels_std = recent_data.std() if np.isnan(labels_std) or labels_std == 0: 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 self.buy_threshold = labels_mean - k_buy * labels_std self.sell_threshold = labels_mean + k_sell * labels_std # 使用 Hyperopt 参数限制阈值 self.buy_threshold = max(self.buy_threshold, self.BUY_THRESHOLD_MIN.value) self.buy_threshold = min(self.buy_threshold, self.BUY_THRESHOLD_MAX.value) 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}] k_buy:{k_buy:.2f}, k_sell:{k_sell:.2f}") logger.info(f"[{pair}] 动态买入阈值:{self.buy_threshold:.4f}, 卖出阈值:{self.sell_threshold:.4f}") if not self.stats_logged: logger.info("===== 所有币对的 labels_mean 和 labels_std 汇总 =====") for p, stats in self.pair_stats.items(): logger.info(f"[{p}] labels_mean:{stats['labels_mean']:.4f}, labels_std:{stats['labels_std']:.4f}") logger.info("==============================================") self.stats_logged = True return dataframe def generate_roi_table(self, params: Dict) -> Dict[int, float]: roi_table = { "0": params.get("roi_t0", self.ROI_T0.value), "30": params.get("roi_t1", self.ROI_T1.value), "60": params.get("roi_t2", self.ROI_T2.value), } return roi_table def roi_space(self): return [ DecimalParameter(0.01, 0.05, name="roi_t0"), DecimalParameter(0.005, 0.02, name="roi_t1"), DecimalParameter(0.0, 0.01, name="roi_t2") ] def trailing_space(self): return [ DecimalParameter(0.01, 0.05, name="trailing_stop_start"), DecimalParameter(0.005, 0.02, name="trailing_stop_distance") ] def leverage_space(self): return [ DecimalParameter(-0.05, -0.01, name="add_position_threshold", default=-0.02), DecimalParameter(0.2, 0.7, name="exit_position_ratio", default=0.5), IntParameter(1, 10, name="cooldown_period_minutes", default=5), IntParameter(1, 3, name="max_entry_position_adjustment", default=2) ] def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: pair = metadata.get('pair', 'Unknown') conditions = [] if "&-price_value_divergence" in dataframe.columns: cond1 = (dataframe["&-price_value_divergence"] < self.buy_threshold) cond2 = (dataframe["volume_z_score"] > 1.5) cond3 = (dataframe["rsi"] < 40) cond4 = (dataframe["close"] <= dataframe["bb_lowerband"]) buy_condition = cond1 & cond2 & cond3 & cond4 conditions.append(buy_condition) logger.debug(f"[{pair}] 买入条件检查 - " f"&-price_value_divergence < {self.buy_threshold:.6f}: {cond1.iloc[-1]}, " f"volume_z_score > 1.5: {cond2.iloc[-1]}, " f"rsi < 40: {cond3.iloc[-1]}, " f"close <= bb_lowerband: {cond4.iloc[-1]}") else: 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, '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}] 买入条件均不满足,未触发入场信号") else: logger.debug(f"[{pair}] 无有效买入条件") return dataframe def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: pair = metadata.get('pair', 'Unknown') conditions = [] if "&-price_value_divergence" in dataframe.columns: cond1 = (dataframe["&-price_value_divergence"] > self.sell_threshold) cond2 = (dataframe["rsi"] > 75) sell_condition = cond1 | cond2 conditions.append(sell_condition) logger.debug(f"[{pair}] 卖出条件检查 - " f"&-price_value_divergence > {self.sell_threshold:.6f}: {cond1.iloc[-1]}, " f"rsi > 75: {cond2.iloc[-1]}") else: 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}] 卖出条件均不满足,未触发出场信号") else: logger.debug(f"[{pair}] 无有效卖出条件") return dataframe def buy_space(self): return [ DecimalParameter(-0.1, -0.01, name="buy_threshold_min"), DecimalParameter(-0.02, -0.001, name="buy_threshold_max"), DecimalParameter(-0.05, -0.01, name="add_position_threshold", default=-0.02), IntParameter(1, 10, name="cooldown_period_minutes", default=5), IntParameter(1, 3, name="max_entry_position_adjustment", default=2) ] def sell_space(self): return [ DecimalParameter(0.001, 0.02, name="sell_threshold_min"), DecimalParameter(0.02, 0.1, name="sell_threshold_max"), DecimalParameter(0.2, 0.7, name="exit_position_ratio", default=0.5) ] def adjust_trade_position(self, trade: Trade, current_time: datetime, current_rate: float, current_profit: float, min_stake: float | None, max_stake: float, current_entry_rate: float, current_exit_rate: float, 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]:(调整金额, 调整原因) """ pair = trade.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 # --- 减仓逻辑 --- 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}") 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 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") return None if self.trailing_stop_enabled: max_rate = trade.max_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},触发全部卖出") return (-trade.stake_amount, f"Trailing stop at {trailing_stop_price:.6f}") 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") 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 # 检查冷却期 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 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: """ 确认交易退出时的逻辑,并记录详细日志 """ 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)) 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: 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: 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: """ 判断市场趋势:bull(牛市)、bear(熊市)、sideways(震荡市) 基于多时间框架的EMA、K线形态和量价关系 """ try: timeframes = ["3m", "15m", "1h"] 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}] 正在计算多时间框架市场趋势") 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 if len(btc_df) < min_candles: logger.warning(f"[{pair}] BTC 数据不足({tf},{len(btc_df)} 根K线),默认震荡市") 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) btc_df["ema_short_slope"] = (btc_df["ema_short"] - btc_df["ema_short"].shift(10)) / btc_df["ema_short"].shift(10) # 价格趋势得分 price_score = 0 if btc_df["close"].iloc[-1] > btc_df["ema_long"].iloc[-1]: price_score += 20 if btc_df["ema_short"].iloc[-1] > btc_df["ema_long"].iloc[-1]: price_score += 20 if btc_df["ema_short_slope"].iloc[-1] > 0.005: price_score += 15 elif btc_df["ema_short_slope"].iloc[-1] < -0.005: price_score -= 15 # 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 if btc_df["stochrsi_k"].iloc[-1] > 80 and btc_df["stochrsi_k"].iloc[-1] < btc_df["stochrsi_d"].iloc[-1]: stochrsi_score -= 15 elif btc_df["stochrsi_k"].iloc[-1] < 20 and btc_df["stochrsi_k"].iloc[-1] > btc_df["stochrsi_d"].iloc[-1]: stochrsi_score += 15 elif btc_df["stochrsi_k"].iloc[-1] > 50: stochrsi_score += 5 elif btc_df["stochrsi_k"].iloc[-1] < 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) 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}") # 加权融合 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)}") # 映射到趋势类别 if final_score >= 70: return 'bull' elif final_score <= 30: return 'bear' else: return 'sideways' except Exception as e: logger.error(f"[{pair}] 获取市场趋势失败:{e}", exc_info=True) return 'sideways'