From e4db999ad9e908a4c75422789936a6af4e3bd84b Mon Sep 17 00:00:00 2001 From: ddrwode <34234@3来 34> Date: Fri, 6 Feb 2026 16:27:40 +0800 Subject: [PATCH] =?UTF-8?q?=E6=97=A5=E5=BF=97=E5=B1=95=E7=A4=BA=E4=BC=98?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../四分之一,五分钟,反手条件充足修改版.py | 374 +++++++++--------- 1 file changed, 187 insertions(+), 187 deletions(-) diff --git a/bitmart/四分之一,五分钟,反手条件充足修改版.py b/bitmart/四分之一,五分钟,反手条件充足修改版.py index a7e570c..b7a290c 100644 --- a/bitmart/四分之一,五分钟,反手条件充足修改版.py +++ b/bitmart/四分之一,五分钟,反手条件充足修改版.py @@ -755,226 +755,226 @@ class BitmartFuturesTransaction: if page_start: # 打开浏览器 - for i in range(5): - if self.openBrowser(): - logger.info(LOG_SYSTEM + "浏览器打开成功") - break - else: - self.ding("打开浏览器失败!", error=True) - return + for i in range(5): + if self.openBrowser(): + logger.info(LOG_SYSTEM + "浏览器打开成功") + break + else: + self.ding("打开浏览器失败!", error=True) + return - # 进入交易页面 - self.page.get("https://derivatives.bitmart.com/zh-CN/futures/ETHUSDT") - self.click_safe('x://button[normalize-space(text()) ="市价"]') + # 进入交易页面 + self.page.get("https://derivatives.bitmart.com/zh-CN/futures/ETHUSDT") + self.click_safe('x://button[normalize-space(text()) ="市价"]') - self.page.ele('x://*[@id="size_0"]').input(vals=25, clear=True) + self.page.ele('x://*[@id="size_0"]').input(vals=25, clear=True) - page_start = False + page_start = False - try: - # 1. 获取K线数据(当前K线和上一根K线) - prev_kline, current_kline = self.get_klines() - if not prev_kline or not current_kline: - logger.warning(LOG_SYSTEM + "获取K线失败,等待重试...") - time.sleep(5) - continue - - # 记录进入新的K线(分块分隔,便于阅读) - current_kline_time = current_kline['id'] - if self.last_kline_time != current_kline_time: - self.last_kline_time = current_kline_time - logger.info("") - log_kline_header(current_kline_time) - - # 2. 获取当前价格 - current_price = self.get_current_price() - if not current_price: - logger.warning(LOG_SYSTEM + "获取价格失败,等待重试...") - time.sleep(2) - continue - - # 3. 每次循环都通过SDK获取真实持仓状态(避免状态不同步导致双向持仓) - if not self.get_position_status(): - logger.warning(LOG_SYSTEM + "获取持仓状态失败,等待重试...") - time.sleep(2) - continue - - logger.debug(f"当前持仓状态: {self.start} (0=无, 1=多, -1=空)") - - # 更新仪表盘左侧数据(供 Rich 展示) try: - self._display_state["price"] = current_price - self._display_state["position"] = self.start - self._display_state["kline_id"] = current_kline_time - self._display_state["unrealized_pnl"] = self.get_unrealized_pnl_usd() - kline_series = self.get_klines_series(35) - ema10, ema20, atr14 = self.get_ema_atr_for_exit(kline_series) - self._display_state["ema10"] = ema10 - self._display_state["atr14"] = atr14 - if getattr(self, "_display_triggers", None): - self._display_state.update(self._display_triggers) - except Exception: - pass + # 1. 获取K线数据(当前K线和上一根K线) + prev_kline, current_kline = self.get_klines() + if not prev_kline or not current_kline: + logger.warning(LOG_SYSTEM + "获取K线失败,等待重试...") + time.sleep(5) + continue - # 3.5 止损/止盈/移动止损 + EMA/ATR 平仓 + 当前K线从极值回落平仓 - if self.start != 0: - # 当前K线从最高/最低点回落平仓:换线重置跟踪,有持仓时更新本K线内最高/最低价并检查 - if self._candle_id_for_high_low != current_kline_time: - self._candle_high_seen = None - self._candle_low_seen = None - self._candle_id_for_high_low = current_kline_time - # 多头:先更新本 K 线最高价(供 ATR 追踪与后续回落逻辑用) - if self.start == 1: - self._candle_high_seen = max(self._candle_high_seen or 0, current_price) - elif self.start == -1: - self._candle_low_seen = min(self._candle_low_seen or float('inf'), current_price) + # 记录进入新的K线(分块分隔,便于阅读) + current_kline_time = current_kline['id'] + if self.last_kline_time != current_kline_time: + self.last_kline_time = current_kline_time + logger.info("") + log_kline_header(current_kline_time) - # 多单:EMA(10) + ATR(14) 平仓 —— 收盘跌破 EMA10 先平;或从最高价回撤 ≥ 1.1×ATR 平 - if self.start == 1 and self.use_ema_atr_exit: + # 2. 获取当前价格 + current_price = self.get_current_price() + if not current_price: + logger.warning(LOG_SYSTEM + "获取价格失败,等待重试...") + time.sleep(2) + continue + + # 3. 每次循环都通过SDK获取真实持仓状态(避免状态不同步导致双向持仓) + if not self.get_position_status(): + logger.warning(LOG_SYSTEM + "获取持仓状态失败,等待重试...") + time.sleep(2) + continue + + logger.debug(f"当前持仓状态: {self.start} (0=无, 1=多, -1=空)") + + # 更新仪表盘左侧数据(供 Rich 展示) + try: + self._display_state["price"] = current_price + self._display_state["position"] = self.start + self._display_state["kline_id"] = current_kline_time + self._display_state["unrealized_pnl"] = self.get_unrealized_pnl_usd() kline_series = self.get_klines_series(35) ema10, ema20, atr14 = self.get_ema_atr_for_exit(kline_series) - if ema10 is not None and current_price < ema10: - logger.info(LOG_POSITION + f"多单 EMA10 平仓 | 价 {current_price:.2f} 跌破 EMA10 {ema10:.2f}") - self.平仓() - self.max_unrealized_pnl_seen = None - self._candle_high_seen = None - time.sleep(3) - continue - if atr14 is not None and self._candle_high_seen and (self._candle_high_seen - current_price) >= self.atr_multiplier * atr14: - logger.info(LOG_POSITION + f"多单 ATR 追踪止盈 | 最高 {self._candle_high_seen:.2f} 当前 {current_price:.2f} 回撤≥{self.atr_multiplier}×ATR={atr14:.2f}") - self.平仓() - self.max_unrealized_pnl_seen = None - self._candle_high_seen = None - time.sleep(3) - continue + self._display_state["ema10"] = ema10 + self._display_state["atr14"] = atr14 + if getattr(self, "_display_triggers", None): + self._display_state.update(self._display_triggers) + except Exception: + pass - # 空单:EMA(10) + ATR(14) 平仓 —— 收盘涨破 EMA10 先平;或从最低价反弹 ≥ 1.1×ATR 平 - if self.start == -1 and self.use_ema_atr_exit: - kline_series = self.get_klines_series(35) - ema10, ema20, atr14 = self.get_ema_atr_for_exit(kline_series) - if ema10 is not None and current_price > ema10: - logger.info(LOG_POSITION + f"空单 EMA10 平仓 | 价 {current_price:.2f} 涨破 EMA10 {ema10:.2f}") - self.平仓() - self.max_unrealized_pnl_seen = None + # 3.5 止损/止盈/移动止损 + EMA/ATR 平仓 + 当前K线从极值回落平仓 + if self.start != 0: + # 当前K线从最高/最低点回落平仓:换线重置跟踪,有持仓时更新本K线内最高/最低价并检查 + if self._candle_id_for_high_low != current_kline_time: + self._candle_high_seen = None self._candle_low_seen = None - time.sleep(3) - continue - if atr14 is not None and self._candle_low_seen and (current_price - self._candle_low_seen) >= self.atr_multiplier * atr14: - logger.info(LOG_POSITION + f"空单 ATR 追踪止盈 | 最低 {self._candle_low_seen:.2f} 当前 {current_price:.2f} 反弹≥{self.atr_multiplier}×ATR={atr14:.2f}") - self.平仓() - self.max_unrealized_pnl_seen = None - self._candle_low_seen = None - time.sleep(3) - continue + self._candle_id_for_high_low = current_kline_time + # 多头:先更新本 K 线最高价(供 ATR 追踪与后续回落逻辑用) + if self.start == 1: + self._candle_high_seen = max(self._candle_high_seen or 0, current_price) + elif self.start == -1: + self._candle_low_seen = min(self._candle_low_seen or float('inf'), current_price) - use_fixed = self.drop_from_high_mode == 'fixed' and self.drop_from_high_to_close and self.drop_from_high_to_close > 0 - use_pct = self.drop_from_high_mode == 'pct_retrace' - if use_fixed or use_pct: - if self.start == 1: # 多头:最高价已在上面更新,这里只做回落判断 - do_close = False - if use_fixed and self._candle_high_seen and current_price <= self._candle_high_seen - self.drop_from_high_to_close: - do_close = True - reason = f"固定回落 {self.drop_from_high_to_close}" - elif use_pct and self._candle_high_seen and current_kline.get('open'): - candle_open = float(current_kline['open']) - rise_pct = (self._candle_high_seen - candle_open) / candle_open * 100 if candle_open > 0 else 0 - if rise_pct >= self.min_rise_pct_to_activate: - drop_trigger_pct = max(self.min_drop_pct_from_high, rise_pct * self.retrace_ratio) - drop_pct = (self._candle_high_seen - current_price) / self._candle_high_seen * 100 if self._candle_high_seen else 0 - if drop_pct >= drop_trigger_pct: - do_close = True - reason = f"涨幅 {rise_pct:.3f}% → 允许回撤 {drop_trigger_pct:.3f}%,实际回撤 {drop_pct:.3f}%" - else: - pass # 涨幅不足,不启用动态回撤 - if do_close: - logger.info(LOG_POSITION + f"多单K线回落平仓 | 最高 {self._candle_high_seen:.2f} 当前 {current_price:.2f} | {reason}") + # 多单:EMA(10) + ATR(14) 平仓 —— 收盘跌破 EMA10 先平;或从最高价回撤 ≥ 1.1×ATR 平 + if self.start == 1 and self.use_ema_atr_exit: + kline_series = self.get_klines_series(35) + ema10, ema20, atr14 = self.get_ema_atr_for_exit(kline_series) + if ema10 is not None and current_price < ema10: + logger.info(LOG_POSITION + f"多单 EMA10 平仓 | 价 {current_price:.2f} 跌破 EMA10 {ema10:.2f}") self.平仓() self.max_unrealized_pnl_seen = None self._candle_high_seen = None time.sleep(3) continue - elif self.start == -1: # 空头:跟踪当前K线最低价,从最低点反弹超过阈值则平仓 - self._candle_low_seen = min(self._candle_low_seen or float('inf'), current_price) - do_close = False - if use_fixed and self._candle_low_seen and current_price >= self._candle_low_seen + self.drop_from_high_to_close: - do_close = True - reason = f"固定反弹 {self.drop_from_high_to_close}" - elif use_pct and self._candle_low_seen and current_kline.get('open'): - candle_open = float(current_kline['open']) - rise_pct = (candle_open - self._candle_low_seen) / candle_open * 100 if candle_open > 0 else 0 # 对空头是“跌幅” - if rise_pct >= self.min_rise_pct_to_activate: - drop_trigger_pct = max(self.min_drop_pct_from_high, rise_pct * self.retrace_ratio) - bounce_pct = (current_price - self._candle_low_seen) / self._candle_low_seen * 100 if self._candle_low_seen else 0 - if bounce_pct >= drop_trigger_pct: - do_close = True - reason = f"跌幅 {rise_pct:.3f}% → 允许反弹 {drop_trigger_pct:.3f}%,实际反弹 {bounce_pct:.3f}%" - if do_close: - logger.info(LOG_POSITION + f"空单K线反弹平仓 | 最低 {self._candle_low_seen:.2f} 当前 {current_price:.2f} | {reason}") + if atr14 is not None and self._candle_high_seen and (self._candle_high_seen - current_price) >= self.atr_multiplier * atr14: + logger.info(LOG_POSITION + f"多单 ATR 追踪止盈 | 最高 {self._candle_high_seen:.2f} 当前 {current_price:.2f} 回撤≥{self.atr_multiplier}×ATR={atr14:.2f}") + self.平仓() + self.max_unrealized_pnl_seen = None + self._candle_high_seen = None + time.sleep(3) + continue + + # 空单:EMA(10) + ATR(14) 平仓 —— 收盘涨破 EMA10 先平;或从最低价反弹 ≥ 1.1×ATR 平 + if self.start == -1 and self.use_ema_atr_exit: + kline_series = self.get_klines_series(35) + ema10, ema20, atr14 = self.get_ema_atr_for_exit(kline_series) + if ema10 is not None and current_price > ema10: + logger.info(LOG_POSITION + f"空单 EMA10 平仓 | 价 {current_price:.2f} 涨破 EMA10 {ema10:.2f}") + self.平仓() + self.max_unrealized_pnl_seen = None + self._candle_low_seen = None + time.sleep(3) + continue + if atr14 is not None and self._candle_low_seen and (current_price - self._candle_low_seen) >= self.atr_multiplier * atr14: + logger.info(LOG_POSITION + f"空单 ATR 追踪止盈 | 最低 {self._candle_low_seen:.2f} 当前 {current_price:.2f} 反弹≥{self.atr_multiplier}×ATR={atr14:.2f}") self.平仓() self.max_unrealized_pnl_seen = None self._candle_low_seen = None time.sleep(3) continue - pnl_usd = self.get_unrealized_pnl_usd() - if pnl_usd is not None: - # 固定止损:亏损达到 3 美元平仓 - if pnl_usd <= self.stop_loss_usd: - logger.info(LOG_POSITION + f"固定止损平仓 | 亏损 {pnl_usd:.2f} 美元") - self.平仓() - self.max_unrealized_pnl_seen = None - time.sleep(3) - continue - # 更新持仓期间最大盈利(用于移动止损) - if self.max_unrealized_pnl_seen is None: - self.max_unrealized_pnl_seen = pnl_usd - else: - self.max_unrealized_pnl_seen = max(self.max_unrealized_pnl_seen, pnl_usd) - # 移动止损:盈利曾达到 activation 后,从最高盈利回撤 trailing_distance 则平仓 - if self.max_unrealized_pnl_seen >= self.trailing_activation_usd: - if pnl_usd < self.max_unrealized_pnl_seen - self.trailing_distance_usd: - logger.info(LOG_POSITION + f"移动止损平仓 | 盈利 {pnl_usd:.2f} 从最高 {self.max_unrealized_pnl_seen:.2f} 回撤≥{self.trailing_distance_usd}$") + use_fixed = self.drop_from_high_mode == 'fixed' and self.drop_from_high_to_close and self.drop_from_high_to_close > 0 + use_pct = self.drop_from_high_mode == 'pct_retrace' + if use_fixed or use_pct: + if self.start == 1: # 多头:最高价已在上面更新,这里只做回落判断 + do_close = False + if use_fixed and self._candle_high_seen and current_price <= self._candle_high_seen - self.drop_from_high_to_close: + do_close = True + reason = f"固定回落 {self.drop_from_high_to_close}" + elif use_pct and self._candle_high_seen and current_kline.get('open'): + candle_open = float(current_kline['open']) + rise_pct = (self._candle_high_seen - candle_open) / candle_open * 100 if candle_open > 0 else 0 + if rise_pct >= self.min_rise_pct_to_activate: + drop_trigger_pct = max(self.min_drop_pct_from_high, rise_pct * self.retrace_ratio) + drop_pct = (self._candle_high_seen - current_price) / self._candle_high_seen * 100 if self._candle_high_seen else 0 + if drop_pct >= drop_trigger_pct: + do_close = True + reason = f"涨幅 {rise_pct:.3f}% → 允许回撤 {drop_trigger_pct:.3f}%,实际回撤 {drop_pct:.3f}%" + else: + pass # 涨幅不足,不启用动态回撤 + if do_close: + logger.info(LOG_POSITION + f"多单K线回落平仓 | 最高 {self._candle_high_seen:.2f} 当前 {current_price:.2f} | {reason}") + self.平仓() + self.max_unrealized_pnl_seen = None + self._candle_high_seen = None + time.sleep(3) + continue + elif self.start == -1: # 空头:跟踪当前K线最低价,从最低点反弹超过阈值则平仓 + self._candle_low_seen = min(self._candle_low_seen or float('inf'), current_price) + do_close = False + if use_fixed and self._candle_low_seen and current_price >= self._candle_low_seen + self.drop_from_high_to_close: + do_close = True + reason = f"固定反弹 {self.drop_from_high_to_close}" + elif use_pct and self._candle_low_seen and current_kline.get('open'): + candle_open = float(current_kline['open']) + rise_pct = (candle_open - self._candle_low_seen) / candle_open * 100 if candle_open > 0 else 0 # 对空头是“跌幅” + if rise_pct >= self.min_rise_pct_to_activate: + drop_trigger_pct = max(self.min_drop_pct_from_high, rise_pct * self.retrace_ratio) + bounce_pct = (current_price - self._candle_low_seen) / self._candle_low_seen * 100 if self._candle_low_seen else 0 + if bounce_pct >= drop_trigger_pct: + do_close = True + reason = f"跌幅 {rise_pct:.3f}% → 允许反弹 {drop_trigger_pct:.3f}%,实际反弹 {bounce_pct:.3f}%" + if do_close: + logger.info(LOG_POSITION + f"空单K线反弹平仓 | 最低 {self._candle_low_seen:.2f} 当前 {current_price:.2f} | {reason}") + self.平仓() + self.max_unrealized_pnl_seen = None + self._candle_low_seen = None + time.sleep(3) + continue + + pnl_usd = self.get_unrealized_pnl_usd() + if pnl_usd is not None: + # 固定止损:亏损达到 3 美元平仓 + if pnl_usd <= self.stop_loss_usd: + logger.info(LOG_POSITION + f"固定止损平仓 | 亏损 {pnl_usd:.2f} 美元") self.平仓() self.max_unrealized_pnl_seen = None time.sleep(3) continue - # 4. 检查信号 - signal = self.check_signal(current_price, prev_kline, current_kline) + # 更新持仓期间最大盈利(用于移动止损) + if self.max_unrealized_pnl_seen is None: + self.max_unrealized_pnl_seen = pnl_usd + else: + self.max_unrealized_pnl_seen = max(self.max_unrealized_pnl_seen, pnl_usd) + # 移动止损:盈利曾达到 activation 后,从最高盈利回撤 trailing_distance 则平仓 + if self.max_unrealized_pnl_seen >= self.trailing_activation_usd: + if pnl_usd < self.max_unrealized_pnl_seen - self.trailing_distance_usd: + logger.info(LOG_POSITION + f"移动止损平仓 | 盈利 {pnl_usd:.2f} 从最高 {self.max_unrealized_pnl_seen:.2f} 回撤≥{self.trailing_distance_usd}$") + self.平仓() + self.max_unrealized_pnl_seen = None + time.sleep(3) + continue + # 4. 检查信号 + signal = self.check_signal(current_price, prev_kline, current_kline) - # 5. 反手过滤:冷却时间 + 最小价差 - if signal and signal[0].startswith('reverse_'): - if not self.can_reverse(current_price, signal[1]): - signal = None + # 5. 反手过滤:冷却时间 + 最小价差 + if signal and signal[0].startswith('reverse_'): + if not self.can_reverse(current_price, signal[1]): + signal = None - # 5.5 开仓频率过滤:仅冷却时间,单根 K 线符合规则可多次开仓 - if signal and signal[0] in ('long', 'short'): - if not self.can_open(current_kline_time): - signal = None - else: - self._current_kline_id_for_open = current_kline_time # 供 execute_trade 成功后记录 + # 5.5 开仓频率过滤:仅冷却时间,单根 K 线符合规则可多次开仓 + if signal and signal[0] in ('long', 'short'): + if not self.can_open(current_kline_time): + signal = None + else: + self._current_kline_id_for_open = current_kline_time # 供 execute_trade 成功后记录 - # 6. 有信号则执行交易 - if signal: - trade_success = self.execute_trade(signal) - if trade_success: - logger.success(LOG_POSITION + f"交易执行完成: {signal[0]} | 当前持仓: {self.start}") - page_start = True - else: - logger.warning(f"交易执行失败或被阻止: {signal[0]}") + # 6. 有信号则执行交易 + if signal: + trade_success = self.execute_trade(signal) + if trade_success: + logger.success(LOG_POSITION + f"交易执行完成: {signal[0]} | 当前持仓: {self.start}") + page_start = True + else: + logger.warning(f"交易执行失败或被阻止: {signal[0]}") - # 短暂等待后继续循环(同一根K线遇到信号就操作) - time.sleep(0.1) + # 短暂等待后继续循环(同一根K线遇到信号就操作) + time.sleep(0.1) - if page_start: - self.page.close() + if page_start: + self.page.close() + time.sleep(5) + + except KeyboardInterrupt: + logger.info(LOG_SYSTEM + "用户中断,程序退出") + break + except Exception as e: + logger.error(f"主循环异常: {e}") time.sleep(5) - - except KeyboardInterrupt: - logger.info(LOG_SYSTEM + "用户中断,程序退出") - break - except Exception as e: - logger.error(f"主循环异常: {e}") - time.sleep(5) finally: if live is not None: try: