diff --git a/.gitignore b/.gitignore index d70536b0..8938f0ec 100644 --- a/.gitignore +++ b/.gitignore @@ -125,5 +125,5 @@ data/ !result/ tools/.env -result/ output.log +result/ diff --git a/freqtrade/templates/freqaiprimer.py b/freqtrade/templates/freqaiprimer.py index 57dac6bd..35dbd6f0 100644 --- a/freqtrade/templates/freqaiprimer.py +++ b/freqtrade/templates/freqaiprimer.py @@ -4,47 +4,38 @@ from functools import reduce import talib.abstract as ta from pandas import DataFrame from technical import qtpylib -from freqtrade.strategy import IStrategy, IntParameter, DecimalParameter -from pandas.core.dtypes.common import is_scalar -import pandas as pd - +from freqtrade.strategy import IStrategy logger = logging.getLogger(__name__) class FreqaiPrimer(IStrategy): - minimal_roi = { - 0: 0.135, - 9: 0.052, - 15: 0.007, - 60: 0 - } - stoploss = -0.263 - trailing_stop = True - trailing_stop_positive = 0.324 - trailing_stop_positive_offset = 0.411 - trailing_only_offset_is_reached = False - max_open_trades = 4 - process_only_new_candles = True - use_exit_signal = True - startup_candle_count: int = 40 - can_short = False + """ + 策略说明: + - 所有交易信号由 FreqAI 动态预测 + - 不使用 Hyperopt 优化任何参数 + - 使用 FreqAI 提供的趋势和波动率信号进行交易决策 + """ - buy_rsi = IntParameter(low=10, high=50, default=30, space="buy", optimize=False, load=True) - sell_rsi = IntParameter(low=50, high=90, default=70, space="sell", optimize=False, load=True) - roi_0 = DecimalParameter(low=0.01, high=0.2, default=0.135, space="roi", optimize=True, load=True) - roi_15 = DecimalParameter(low=0.005, high=0.1, default=0.052, space="roi", optimize=True, load=True) - roi_30 = DecimalParameter(low=0.001, high=0.05, default=0.007, space="roi", optimize=True, load=True) - stoploss_param = DecimalParameter(low=-0.35, high=-0.1, default=-0.263, space="stoploss", optimize=True, load=True) - trailing_stop_positive_param = DecimalParameter(low=0.1, high=0.5, default=0.324, space="trailing", optimize=True, load=True) - trailing_stop_positive_offset_param = DecimalParameter(low=0.2, high=0.6, default=0.411, space="trailing", optimize=True, load=True) - tema_period = IntParameter(low=5, high=20, default=9, space="buy", optimize=True, load=True) + plot_config = { + "main_plot": {}, + "subplots": { + "Signals": { + "enter_long": {"color": "green"}, + "exit_long": {"color": "red"} + }, + "FreqAI Predictions": { + "&-buy_signal": {"color": "blue"}, + "&-sell_signal": {"color": "orange"}, + "&-volatility_forecast": {"color": "purple"} + } + } + } freqai_info = { - "model": "LightGBMRegressorMultiTarget", + "model": "LightGBMClassifier", "feature_parameters": { "include_timeframes": ["5m", "15m", "1h"], - "include_corr_pairlist": [], - "label_period_candles": 12, + "label_period_candles": 24, "include_shifted_candles": 3, }, "data_split_parameters": { @@ -52,357 +43,100 @@ class FreqaiPrimer(IStrategy): "shuffle": False, }, "model_training_parameters": { - "n_estimators": 500, - "learning_rate": 0.01, - "num_leaves": 64, - "max_depth": 8, + "n_estimators": 200, + "learning_rate": 0.05, + "num_leaves": 31, "verbose": -1, }, } - plot_config = { - "main_plot": {}, - "subplots": { - "&-buy_rsi": {"&-buy_rsi": {"color": "green"}}, - "&-sell_rsi": {"&-sell_rsi": {"color": "red"}}, - "&-stoploss": {"&-stoploss": {"color": "purple"}}, - "&-roi_0": {"&-roi_0": {"color": "orange"}}, - "do_predict": {"do_predict": {"color": "brown"}}, - }, - } - - def __init__(self, config: dict): - super().__init__(config) - self.cycle_trade_pair = None # 跟踪当前周期的下单币对 - self.last_cycle_time = None # 记录上一个周期时间 - def bot_loop_start(self, current_time, **kwargs) -> None: - """ - Reset cycle_trade_pair at the start of each new cycle. - """ - try: - # 检查是否进入新周期(基于 timeframe,如 5m) - if self.last_cycle_time is None or current_time >= self.last_cycle_time + pd.Timedelta(self.timeframe): - self.cycle_trade_pair = None - self.last_cycle_time = current_time - logger.debug(f"New cycle started at {current_time}, reset cycle_trade_pair") - except Exception as e: - logger.error(f"Error in bot_loop_start: {str(e)}") - def clean_dataframe(self, dataframe: DataFrame, columns: list = None) -> DataFrame: - try: - if columns: - valid_columns = [col for col in columns if col in dataframe.columns] - if not valid_columns: - logger.warning(f"None of the specified columns {columns} exist in DataFrame. Skipping cleaning.") - return dataframe - for col in valid_columns: - logger.debug(f"Cleaning column {col}: NaN count before = {dataframe[col].isna().sum()}") - dataframe[col] = dataframe[col].replace([np.inf, -np.inf], 0).ffill().fillna(0) - logger.debug(f"NaN count after = {dataframe[col].isna().sum()}") - else: - logger.debug(f"Cleaning entire DataFrame: NaN count before = {dataframe.isna().sum().sum()}") - dataframe = dataframe.replace([np.inf, -np.inf], 0).ffill().fillna(0) - logger.debug(f"NaN count after = {dataframe.isna().sum().sum()}") - return dataframe - except Exception as e: - logger.error(f"Data cleaning failed: {str(e)}") - return dataframe # 返回原始 DataFrame,避免策略崩溃 - def feature_engineering_expand_all(self, dataframe: DataFrame, period: int, metadata: dict, **kwargs) -> DataFrame: - try: - dataframe = self.clean_dataframe(dataframe, columns=["close", "volume", "open", "high", "low"]) - dataframe["%-rsi-period"] = ta.RSI(dataframe, timeperiod=period) - dataframe["%-mfi-period"] = ta.MFI(dataframe, timeperiod=period) - dataframe["%-sma-period"] = ta.SMA(dataframe, timeperiod=period) - dataframe["%-ema-period"] = ta.EMA(dataframe, timeperiod=period) - dataframe["%-adx-period"] = ta.ADX(dataframe, timeperiod=period) - bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=period, stds=2.2) - dataframe["bb_lowerband-period"] = bollinger["lower"] - dataframe["bb_middleband-period"] = bollinger["mid"] - dataframe["bb_upperband-period"] = bollinger["upper"] - dataframe["%-bb_width-period"] = ( - dataframe["bb_upperband-period"] - dataframe["bb_lowerband-period"] - ) / dataframe["bb_middleband-period"] - dataframe["%-close-bb_lower-period"] = dataframe["close"] / dataframe["bb_lowerband-period"] - dataframe["%-roc-period"] = ta.ROC(dataframe, timeperiod=period) - dataframe["%-relative_volume-period"] = ( - dataframe["volume"] / dataframe["volume"].rolling(period).mean() - ) - dataframe = self.clean_dataframe(dataframe, columns=["%-rsi-period", "%-mfi-period", "%-sma-period", - "%-ema-period", "%-adx-period", "bb_lowerband-period", - "bb_middleband-period", "bb_upperband-period", - "%-bb_width-period", "%-close-bb_lower-period", - "%-roc-period", "%-relative_volume-period"]) - return dataframe - except Exception as e: - logger.error(f"Error in feature_engineering_expand_all for {metadata['pair']}: {str(e)}") - return 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) - def feature_engineering_expand_basic(self, dataframe: DataFrame, metadata: dict, **kwargs) -> DataFrame: - try: - dataframe = self.clean_dataframe(dataframe, columns=["close", "volume", "open", "high", "low"]) - dataframe["%-pct-change"] = dataframe["close"].pct_change() - dataframe["%-raw_volume"] = dataframe["volume"] - dataframe["%-raw_price"] = dataframe["close"] - dataframe = self.clean_dataframe(dataframe, columns=["%-pct-change", "%-raw_volume", "%-raw_price"]) - return dataframe - except Exception as e: - logger.error(f"Error in feature_engineering_expand_basic for {metadata['pair']}: {str(e)}") - return dataframe + 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"] - def feature_engineering_standard(self, dataframe: DataFrame, metadata: dict, **kwargs) -> DataFrame: - try: - dataframe = self.clean_dataframe(dataframe, columns=["close", "volume", "open", "high", "low"]) - if len(dataframe["close"]) < 20: - logger.warning(f"数据不足 {len(dataframe)} 根 K 线,%-volatility 可能不完整") - dataframe["%-day_of_week"] = dataframe["date"].dt.dayofweek - dataframe["%-hour_of_day"] = dataframe["date"].dt.hour - dataframe["%-volatility"] = dataframe["close"].pct_change().rolling(20, min_periods=1).std() - dataframe["%-volatility"] = dataframe["%-volatility"].replace([np.inf, -np.inf], 0).ffill().fillna(0) - dataframe = self.clean_dataframe(dataframe, columns=["%-day_of_week", "%-hour_of_day", "%-volatility"]) - return dataframe - except Exception as e: - logger.error(f"Error in feature_engineering_standard for {metadata['pair']}: {str(e)}") - return dataframe + dataframe["%-roc-period"] = ta.ROC(dataframe, timeperiod=period) + return dataframe def set_freqai_targets(self, dataframe: DataFrame, metadata: dict, **kwargs) -> DataFrame: - try: - logger.info(f"设置 FreqAI 目标,交易对:{metadata['pair']}") - if "close" not in dataframe.columns: - logger.error("数据框缺少必要的 'close' 列") - raise ValueError("数据框缺少必要的 'close' 列") - label_period = self.freqai_info["feature_parameters"]["label_period_candles"] - if "%-volatility" not in dataframe.columns: - logger.warning("缺少 %-volatility 列,强制重新生成") - dataframe["%-volatility"] = dataframe["close"].pct_change().rolling(20, min_periods=1).std() - dataframe["%-volatility"] = dataframe["%-volatility"].replace([np.inf, -np.inf], 0).ffill().fillna(0) - dataframe["&-buy_rsi"] = ta.RSI(dataframe, timeperiod=14).rolling(window=label_period).mean().ffill().bfill() - for col in ["&-buy_rsi", "%-volatility"]: - dataframe[col] = dataframe[col].replace([np.inf, -np.inf], 0).ffill().fillna(0) - if dataframe[col].isna().any(): - logger.warning(f"目标列 {col} 仍包含 NaN,数据预览:\n{dataframe[col].tail(10)}") - logger.info(f"目标列预览:\n{dataframe[['&-buy_rsi']].head().to_string()}") - return dataframe - except Exception as e: - logger.error(f"创建 FreqAI 目标失败:{str(e)}") - return dataframe + """ + 使用历史窗口预测未来趋势和波动率作为辅助信号, + 避免使用任何未来数据(如 shift(-N)) + """ + + label_period = self.freqai_info["feature_parameters"]["label_period_candles"] + + # 1. 趋势强度:使用过去 N 根 K 线的收益率判断当前趋势 + dataframe["&-trend_strength"] = dataframe["close"].pct_change(label_period) + + # 2. 波动率预测:使用过去 N 根 K 线的价格变动绝对值 + dataframe["&-volatility_forecast"] = dataframe["close"].pct_change(label_period).abs() + + # 3. 动态 ROI 目标:基于趋势强度缩放 + dataframe["&-roi_target"] = np.where( + dataframe["&-trend_strength"] > 0, + dataframe["&-trend_strength"] * 1.5, + 0.01 # 最小 ROI + ) + + # 4. 动态止损目标:基于波动率缩放 + dataframe["&-stoploss_target"] = -dataframe["&-volatility_forecast"] * 1.2 + + # 5. 市场状态识别:判断当前是震荡、趋势、超买还是超卖 + if "bb_upperband-period" in dataframe.columns and "bb_lowerband-period" in dataframe.columns: + dataframe["&-market_condition"] = np.select([ + (dataframe["close"] > dataframe["bb_upperband-period"]), + (dataframe["close"] < dataframe["bb_lowerband-period"]), + (dataframe["&-trend_strength"] > 0.03), + ], [ + "overbought", + "oversold", + "strong_up", + ], default="sideways") + + # 买入信号(必须存在) + dataframe["&-buy_signal"] = np.where(dataframe["&-trend_strength"] > 0.01, 1, 0) + + # 卖出信号(必须存在) + dataframe["&-sell_signal"] = np.where(dataframe["&-trend_strength"] < -0.01, 1, 0) + + + return dataframe def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: - try: - logger.debug(f"Pre-cleaning input DataFrame for {metadata['pair']}: columns={list(dataframe.columns)}") - dataframe = self.clean_dataframe(dataframe, columns=["close", "volume", "open", "high", "low"]) - logger.info(f"Processing pair: {metadata['pair']}") - logger.debug(f"Input columns before FreqAI: {list(dataframe.columns)}, shape={dataframe.shape}") - - # FreqAI 预测 - dataframe = self.freqai.start(dataframe, metadata, self) - logger.debug(f"Columns after FreqAI: {list(dataframe.columns)}, shape={dataframe.shape}") - - # 技术指标 - dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14) - bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2) - dataframe["bb_lowerband"] = bollinger["lower"] - dataframe["bb_middleband"] = bollinger["mid"] - dataframe["bb_upperband"] = bollinger["upper"] - dataframe["tema"] = ta.TEMA(dataframe, timeperiod=self.tema_period.value) - dataframe["atr"] = ta.ATR(dataframe["high"], dataframe["low"], dataframe["close"], timeperiod=14) - - # 标签生成 - label_period = self.freqai_info["feature_parameters"]["label_period_candles"] - dataframe["up_or_down"] = np.where( - dataframe["close"].rolling(window=label_period).mean() > dataframe["close"], 1, 0 - ) - - # FreqAI 预测后处理 - if "&-buy_rsi" in dataframe.columns: - if "%-volatility" not in dataframe.columns: - logger.warning("Missing %-volatility column, regenerating") - dataframe["%-volatility"] = dataframe["close"].pct_change().rolling(20, min_periods=1).std() - dataframe["%-volatility"] = dataframe["%-volatility"].replace([np.inf, -np.inf], 0).ffill().fillna(0) - dataframe["&-sell_rsi"] = dataframe["&-buy_rsi"] + 30 - dataframe["&-stoploss"] = self.stoploss - (dataframe["%-volatility"] * 5).clip(-0.05, 0.05) - dataframe["&-roi_0"] = (dataframe["close"].rolling(window=label_period).mean() / dataframe["close"] - 1).clip(0, 0.2) - dataframe = self.clean_dataframe(dataframe, columns=["&-buy_rsi", "&-sell_rsi", "&-stoploss", "&-roi_0", "%-volatility"]) - dataframe["buy_rsi_pred"] = dataframe["&-buy_rsi"].rolling(5).mean().clip(10, 50) - dataframe["sell_rsi_pred"] = dataframe["&-sell_rsi"].rolling(5).mean().clip(50, 90) - dataframe["stoploss_pred"] = dataframe["&-stoploss"].clip(-0.35, -0.1) - dataframe["roi_0_pred"] = dataframe["&-roi_0"].clip(0.01, 0.2) - - # 处理缺失值 - for col in ["buy_rsi_pred", "sell_rsi_pred", "stoploss_pred", "roi_0_pred"]: - if dataframe[col].isna().any(): - logger.warning(f"Column {col} contains NaN values, filling with mean") - dataframe[col] = dataframe[col].ffill().fillna(dataframe[col].mean()) - - dataframe["trailing_stop_positive"] = (dataframe["roi_0_pred"] * 0.5).clip(0.01, 0.3) - dataframe["trailing_stop_positive_offset"] = (dataframe["roi_0_pred"] * 0.75).clip(0.02, 0.4) - - # 更新动态参数 - self.buy_rsi.value = float(dataframe["buy_rsi_pred"].iloc[-1]) - self.sell_rsi.value = float(dataframe["sell_rsi_pred"].iloc[-1]) - self.stoploss = float(self.stoploss_param.value) - self.minimal_roi = { - 0: float(self.roi_0.value), - 15: float(self.roi_15.value), - 30: float(self.roi_30.value), - 60: 0 - } - self.trailing_stop_positive = float(self.trailing_stop_positive_param.value) - self.trailing_stop_positive_offset = float(self.trailing_stop_positive_offset_param.value) - logger.info(f"Dynamic parameters: buy_rsi={self.buy_rsi.value}, sell_rsi={self.sell_rsi.value}, " - f"stoploss={self.stoploss}, trailing_stop_positive={self.trailing_stop_positive}") - else: - logger.warning(f"&-buy_rsi column missing, skipping FreqAI prediction logic. Check model training or prediction files.") - dataframe["buy_rsi_pred"] = self.buy_rsi.default - dataframe["sell_rsi_pred"] = self.sell_rsi.default - dataframe["stoploss_pred"] = self.stoploss_param.default - dataframe["roi_0_pred"] = self.roi_0.default - dataframe["trailing_stop_positive"] = self.trailing_stop_positive_param.default - dataframe["trailing_stop_positive_offset"] = self.trailing_stop_positive_offset_param.default - - # 清理生成的列 - generated_columns = [ - "rsi", "bb_lowerband", "bb_middleband", "bb_upperband", "tema", "atr", - "up_or_down", "buy_rsi_pred", "sell_rsi_pred", "stoploss_pred", "roi_0_pred", - "trailing_stop_positive", "trailing_stop_positive_offset" - ] - dataframe = self.clean_dataframe(dataframe, columns=generated_columns) - - logger.info(f"up_or_down value counts:\n{dataframe['up_or_down'].value_counts().to_string()}") - logger.info(f"do_predict value counts:\n{dataframe['do_predict'].value_counts().to_string()}") - logger.debug(f"Final columns: {list(dataframe.columns)}, shape={dataframe.shape}") - return dataframe - except Exception as e: - logger.error(f"Error processing indicators for {metadata['pair']}: {str(e)}") - return dataframe # 返回未修改的 DataFrame,避免中断 + logger.info(f"[{metadata['pair']}] 当前可用列: {list(dataframe.columns)}") + dataframe = self.freqai.start(dataframe, metadata, self) + + # FreqAI 提供的预测列 + if "&-trend" in dataframe.columns: + dataframe["in_uptrend"] = dataframe["&-trend"] > 0.5 + + return dataframe def populate_entry_trend(self, df: DataFrame, metadata: dict) -> DataFrame: - try: - # 验证输入列 - required_columns = ["rsi", "tema", "volume", "buy_rsi_pred", "do_predict", "bb_middleband", "close", "&-buy_rsi"] - missing_cols = [col for col in required_columns if col not in df.columns] - if missing_cols: - logger.error(f"Missing columns for {metadata['pair']}: {missing_cols}") - return df + conditions = [] - # 处理 NaN - if df[required_columns].isna().any().any(): - logger.warning(f"NaN values in {metadata['pair']}: {df[required_columns].isna().any()}") - df[required_columns] = df[required_columns].ffill().fillna(0) + if "&-buy_signal" in df.columns: + conditions.append(df["&-buy_signal"] > 0.5) + else: + logger.warning("⚠️ &-buy_signal 列缺失,跳过该条件") - # 计算 entry_score - df["entry_score"] = ( - (50 - df["rsi"]) * 0.5 + # RSI 超卖权重 - (df["volume"] / df["volume"].rolling(20, min_periods=1).mean()) * 0.3 + # 成交量活跃 - df["&-buy_rsi"] * 0.2 # FreqAI 预测 - ) - - # 确保 entry_score 是标量 - if not df["entry_score"].apply(is_scalar).all(): - logger.error(f"Non-scalar values in entry_score for {metadata['pair']}: {df['entry_score'].head()}") - df["entry_score"] = df["entry_score"].apply(lambda x: x[0] if isinstance(x, (list, np.ndarray)) else x).astype(float) - - # 买入条件(优化为 5 个,触发率 0.35%-0.42%) - enter_long_conditions = [ - qtpylib.crossed_above(df["rsi"], df["buy_rsi_pred"]), # RSI 上穿 FreqAI 预测 - df["tema"] > df["tema"].shift(1), # TEMA 上行(趋势确认) - df["volume"] > df["volume"].rolling(20, min_periods=1).mean() * 1.1, # 成交量略高于均量 - df["do_predict"] == 1, # FreqAI 预测有效 - df["rsi"] < 45 # RSI 放宽到 < 45 - ] - - # 合并条件 - if enter_long_conditions: - condition = reduce(lambda x, y: x & y, enter_long_conditions) - df.loc[condition, "enter_long"] = 1 - df.loc[condition, "enter_tag"] = "long" - df.loc[condition, "entry_score"] = df.loc[condition, "entry_score"] - df["enter_long"] = df["enter_long"].fillna(0).astype(int) - df["enter_tag"] = df["enter_tag"].fillna("") - df["entry_score"] = df["entry_score"].fillna(0.0).astype(float) - else: - logger.warning(f"No valid entry conditions for {metadata['pair']}") - df["enter_long"] = 0 - df["enter_tag"] = "" - df["entry_score"] = 0.0 - - logger.debug(f"Entry signals for {metadata['pair']}: {df['enter_long'].sum()} signals") - return df - except Exception as e: - logger.error(f"Error in populate_entry_trend for {metadata['pair']}: {str(e)}") + if len(conditions) == 0: return df + + df.loc[reduce(lambda x, y: x & y, conditions), 'enter_long'] = 1 + return df + def populate_exit_trend(self, df: DataFrame, metadata: dict) -> DataFrame: - try: - exit_long_conditions = [ - qtpylib.crossed_above(df["rsi"], df["sell_rsi_pred"]), - (df["close"] < df["close"].shift(1) * 0.97) | (df["rsi"] > 80), - df["volume"] > 0, - (df["do_predict"] == 1) | (df["do_predict"] == 0), - (df["up_or_down"] == 0) | (df["rsi"] > 70) - ] - if exit_long_conditions: - df.loc[ - reduce(lambda x, y: x & y, exit_long_conditions), - "exit_long" - ] = 1 - return df - except Exception as e: - logger.error(f"Error in populate_exit_trend for {metadata['pair']}: {str(e)}") - return df - - def confirm_trade_entry( - self, pair: str, order_type: str, amount: float, rate: float, - time_in_force: str, current_time, entry_tag, side: str, **kwargs - ) -> bool: - try: - df, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) - if df is None or df.empty: - logger.warning(f"无法获取 {pair} 的分析数据,拒绝交易") - return False - - last_candle = df.iloc[-1].squeeze() - if "close" not in last_candle or np.isnan(last_candle["close"]): - logger.warning(f"{pair} 的最新 K 线缺少有效 close 价格,拒绝交易") - return False - - if side == "long": - # 检查当前周期是否已有下单 - if self.cycle_trade_pair is not None: - logger.debug(f"周期内已有下单 {self.cycle_trade_pair},拒绝 {pair} 的买入") - return False - - # 收集所有交易对的信号和分数 - signals = [] - for p in self.config["exchange"]["pair_whitelist"]: - p_df, _ = self.dp.get_analyzed_dataframe(p, self.timeframe) - if p_df is not None and not p_df.empty and "enter_long" in p_df.columns: - if p_df["enter_long"].iloc[-1] == 1: - score = p_df["entry_score"].iloc[-1] if "entry_score" in p_df.columns else -float('inf') - signals.append((p, score)) - - # 如果有信号,选择得分最高的币对 - if signals: - signals = sorted(signals, key=lambda x: x[1], reverse=True) - top_pair, top_score = signals[0] - if pair != top_pair: - logger.debug(f"{pair} 不是最高得分币对 {top_pair} (score={top_score:.2f}),拒绝买入") - return False - else: - logger.debug(f"无有效信号,拒绝 {pair} 的买入") - return False - - # 滑点检查 - max_rate = last_candle["close"] * (1 + 0.0025) # 0.25% 滑点阈值 - if rate > max_rate: - logger.debug(f"拒绝 {pair} 的买入,价格 {rate} 超过最大允许价格 {max_rate}") - return False - - # 记录下单币对 - self.cycle_trade_pair = pair - logger.info(f"确认 {pair} 的买入,周期内最优信号 (score={last_candle['entry_score']:.2f})") - return True - elif side == "short": - logger.warning(f"{pair} 尝试做空,但策略不支持做空 (can_short={self.can_short})") - return False - except Exception as e: - logger.error(f"确认 {pair} 交易时出错:{str(e)}") - return False + conditions = [ + df["&-sell_signal"] > 0.5, + ] + df.loc[reduce(lambda x, y: x & y, conditions), 'exit_long'] = 1 + return df