diff --git a/bitmart/均线自动化开单.py b/bitmart/均线自动化开单.py index 60a57d9..66700f4 100644 --- a/bitmart/均线自动化开单.py +++ b/bitmart/均线自动化开单.py @@ -2,6 +2,7 @@ import time import uuid import datetime import requests +from typing import Tuple from tqdm import tqdm from loguru import logger @@ -107,6 +108,19 @@ class StrategyConfig: post_sl_mult_max: float = 1.16 post_sl_vol_alpha: float = 0.20 + # ========================================================= + # ✅ 正常平仓条件:价格回归到EMA附近时平仓 + # ========================================================= + normal_exit_threshold: float = 0.0003 # 0.03%,价格回归到EMA附近时平仓 + normal_exit_min_profit: float = 0.0002 # 0.02%,正常平仓最小盈利要求 + + # ========================================================= + # ✅ 手续费配置(固定10u,30倍杠杆) + # ========================================================= + fixed_margin: float = 10.0 # 固定每单10u保证金 + platform_fee_rate: float = 0.0005 # 平台手续费:开仓价值的万分之五 + rebate_rate: float = 0.90 # 返佣比例:90% + class BitmartFuturesMeanReversionBot: def __init__(self, cfg: StrategyConfig, bit_id=None): @@ -132,6 +146,10 @@ class BitmartFuturesMeanReversionBot: self.entry_ts = None self.last_exit_ts = 0 + # 开仓信息(用于手续费计算) + self.entry_margin = None # 开仓保证金(固定10u) + self.entry_position_value = None # 开仓时的仓位价值(保证金 * 杠杆) + # 日内权益基准 self.day_start_equity = None self.trading_enabled = True @@ -532,20 +550,30 @@ class BitmartFuturesMeanReversionBot: # ----------------- 下单 ----------------- def calculate_size(self, price: float) -> int: + """ + 计算仓位大小 + 固定每单10u保证金,30倍杠杆 + """ bal = self.get_assets_available() - if bal < 10: + if bal < self.cfg.fixed_margin: + logger.warning(f"余额不足:{bal:.2f} USDT < {self.cfg.fixed_margin} USDT") return 0 - margin = bal * self.cfg.risk_percent + # 固定保证金10u + margin = self.cfg.fixed_margin lev = int(self.cfg.leverage) # ⚠️ 沿用你的原假设:1张≈0.001ETH + # 仓位价值 = 保证金 * 杠杆 = 10 * 30 = 300u + # size = 仓位价值 / (价格 * 0.001) size = int((margin * lev) / (price * 0.001)) size = max(self.cfg.min_size, size) size = min(self.cfg.max_size, size) + + logger.info(f"计算仓位:保证金={margin}u, 杠杆={lev}x, 仓位价值={margin * lev}u, size={size}") return size - def place_market_order(self, side: int, size: int, ) -> bool: + def place_market_order(self, side: int, size: int) -> bool: """ 【下单函数】实际执行下单操作 side: 1=开多, 4=开空, 2=平多, 3=平空 @@ -554,43 +582,221 @@ class BitmartFuturesMeanReversionBot: return False try: - + # 开多单 if side == 1: self.click_safe('x://button[normalize-space(text()) ="市价"]') - self.page.ele('x://*[@id="size_0"]').input(size) - self.click_safe('x://span[normalize-space(text()) ="卖出/做空"]') - elif side == 4: self.click_safe('x://button[normalize-space(text()) ="市价"]') - self.page.ele('x://*[@id="size_0"]').input(size) + self.click_safe('x://button[normalize-space(text()) ="市价"]') + self.page.ele('x://*[@id="size_0"]').input(size, clear=True) self.click_safe('x://span[normalize-space(text()) ="买入/做多"]') - else: - self.click_safe('x://span[normalize-space(text()) ="市价"]') - - # if resp.get("code") == 1000: + logger.info(f"✅ 开多单: size={size}") return True - # self.ding(f"下单失败: {resp}", error=True) - return False + # 开空单 + elif side == 4: + self.click_safe('x://button[normalize-space(text()) ="市价"]') + self.click_safe('x://button[normalize-space(text()) ="市价"]') + self.click_safe('x://button[normalize-space(text()) ="市价"]') + self.page.ele('x://*[@id="size_0"]').input(size, clear=True) + self.click_safe('x://span[normalize-space(text()) ="卖出/做空"]') + logger.info(f"✅ 开空单: size={size}") + return True - except APIException as e: - logger.error(f"API下单异常: {e}") - self.ding(f"API下单异常: {e}", error=True) - return False + # 平多单(平多 = 卖出) + elif side == 2: + self.click_safe('x://button[normalize-space(text()) ="市价"]') + time.sleep(0.3) # 等待界面响应 + # 平仓时size可以设置大一些确保全部平仓 + self.page.ele('x://*[@id="size_0"]').input(size, clear=True) + time.sleep(0.3) + self.click_safe('x://span[normalize-space(text()) ="卖出/做空"]') + logger.info(f"✅ 平多单操作执行: size={size}") + return True + + # 平空单(平空 = 买入) + elif side == 3: + self.click_safe('x://button[normalize-space(text()) ="市价"]') + time.sleep(0.3) # 等待界面响应 + self.page.ele('x://*[@id="size_0"]').input(size, clear=True) + time.sleep(0.3) + self.click_safe('x://span[normalize-space(text()) ="买入/做多"]') + logger.info(f"✅ 平空单操作执行: size={size}") + return True + + else: + logger.error(f"未知的订单方向: side={side}") + return False except Exception as e: - logger.error(f"下单未知异常: {e}") - self.ding(f"下单未知异常: {e}", error=True) + logger.error(f"下单异常: {e}") + self.ding(f"下单异常: {e}", error=True) return False - def close_position_all(self): + def calculate_fee(self, position_value: float) -> float: + """ + 计算手续费 + 平台手续费 = 仓位价值 * 0.0005(万分之五) + 实际手续费 = 平台手续费 * (1 - 返佣比例) = 平台手续费 * 0.1 + """ + platform_fee = position_value * self.cfg.platform_fee_rate + actual_fee = platform_fee * (1 - self.cfg.rebate_rate) + return actual_fee + + def calculate_net_pnl(self, price: float) -> Tuple[float, float, float]: + """ + 计算扣除手续费后的净盈亏 + 返回: (净盈亏比例, 总手续费, 毛盈亏比例) + + 注意:在杠杆交易中,盈亏比例 = 杠杆倍数 * 价格变动比例 + 例如:30倍杠杆,价格涨1%,实际盈亏是30% + """ + if self.pos == 0 or self.entry_price is None or self.entry_margin is None or self.entry_position_value is None: + return 0.0, 0.0, 0.0 + + leverage = int(self.cfg.leverage) + + # 计算价格变动比例 if self.pos == 1: - ok = self.place_market_order(3, 999999) - if ok: - self.pos = 0 - elif self.pos == -1: - ok = self.place_market_order(2, 999999) - if ok: + price_change_ratio = (price - self.entry_price) / self.entry_price + else: + price_change_ratio = (self.entry_price - price) / self.entry_price + + # 计算毛盈亏比例(考虑杠杆) + gross_pnl_ratio = leverage * price_change_ratio + + # 计算开仓手续费 + entry_fee = self.calculate_fee(self.entry_position_value) + + # 计算平仓手续费(使用当前价格计算平仓时的仓位价值) + # 平仓时的仓位价值 ≈ 开仓时的仓位价值(假设size不变) + exit_position_value = self.entry_position_value + exit_fee = self.calculate_fee(exit_position_value) + + # 总手续费 + total_fee = entry_fee + exit_fee + + # 手续费相对于保证金的比率 + fee_ratio = total_fee / self.entry_margin + + # 净盈亏 = 毛盈亏 - 手续费比率 + net_pnl_ratio = gross_pnl_ratio - fee_ratio + + return net_pnl_ratio, total_fee, gross_pnl_ratio + + def close_position_all(self) -> bool: + """ + 平掉所有持仓 + 重试机制:最多尝试3次,每次平仓后通过SDK验证是否成功 + 返回: True=平仓成功, False=平仓失败 + """ + if self.pos == 0: + logger.info("当前无持仓,无需平仓") + return True + + max_retries = 3 + retry_delay = 1.0 # 每次重试间隔1秒 + + for attempt in range(1, max_retries + 1): + logger.info(f"平仓尝试 {attempt}/{max_retries}...") + + # 记录平仓前的持仓状态 + old_pos = self.pos + + # 执行平仓操作 + if self.pos == 1: + # 平多单,使用side=2 + ok = self.place_market_order(2, 999999) + if not ok: + logger.warning(f"平多单操作失败 (尝试 {attempt}/{max_retries})") + if attempt < max_retries: + time.sleep(retry_delay) + continue + else: + self.ding(f"平多单失败:已重试{max_retries}次仍失败", error=True) + return False + elif self.pos == -1: + # 平空单,使用side=3 + ok = self.place_market_order(3, 999999) + if not ok: + logger.warning(f"平空单操作失败 (尝试 {attempt}/{max_retries})") + if attempt < max_retries: + time.sleep(retry_delay) + continue + else: + self.ding(f"平空单失败:已重试{max_retries}次仍失败", error=True) + return False + else: + logger.info("持仓状态异常,无需平仓") + return True + + # 等待订单执行 + time.sleep(1.5) # 等待订单执行 + + # 通过SDK验证是否平仓成功 + verify_success = self._verify_position_closed(old_pos) + + if verify_success: + # 平仓成功,清空状态 self.pos = 0 + self.entry_margin = None + self.entry_position_value = None + logger.success(f"✅ 平仓成功 (尝试 {attempt}/{max_retries})") + return True + else: + # 平仓失败,准备重试 + logger.warning(f"平仓验证失败,持仓仍存在 (尝试 {attempt}/{max_retries})") + if attempt < max_retries: + time.sleep(retry_delay) + # 重新获取持仓状态 + self.get_position_status() + else: + self.ding(f"平仓失败:已重试{max_retries}次,持仓仍未平掉", error=True) + return False + + return False + + def _verify_position_closed(self, expected_old_pos: int) -> bool: + """ + 验证持仓是否已平仓 + 通过SDK查询持仓状态,确认是否真的平仓成功 + 返回: True=平仓成功, False=仍有持仓 + """ + try: + # 调用SDK查询持仓状态 + resp = self.contractAPI.get_position(contract_symbol=self.cfg.contract_symbol)[0] + + if resp.get("code") != 1000: + logger.warning(f"查询持仓状态失败: {resp}") + return False + + positions = resp.get("data", []) + + # 如果没有持仓,说明平仓成功 + if not positions or len(positions) == 0: + logger.info("✅ SDK验证:持仓已平仓") + return True + + # 检查持仓是否还存在 + # position_type: 1=多, 2=空 (根据get_position_status的逻辑) + for p in positions: + position_type = p.get("position_type", 0) + # 根据get_position_status的逻辑:1=多(pos=1), 其他=空(pos=-1) + current_pos = 1 if position_type == 1 else -1 + + if current_pos == expected_old_pos: + # 持仓仍然存在(与平仓前的方向一致) + logger.warning( + f"⚠️ SDK验证:持仓仍存在 (position_type={position_type}, 方向={'多' if current_pos == 1 else '空'})") + return False + + # 持仓已改变或不存在(平仓成功) + logger.info("✅ SDK验证:持仓已平仓或已改变") + return True + + except Exception as e: + logger.error(f"验证持仓状态异常: {e}") + # 验证失败时,保守处理:认为可能未平仓 + return False # ----------------- 止损后机制 ----------------- def _reentry_penalty_active(self, dev: float, entry_dev: float) -> bool: @@ -674,7 +880,11 @@ class BitmartFuturesMeanReversionBot: self.pos = 1 self.entry_price = price self.entry_ts = time.time() - self.ding(f"✅开多:dev={dev * 100:.3f}% size={size} entry={price:.2f}") + # 记录开仓信息(用于手续费计算) + self.entry_margin = self.cfg.fixed_margin + self.entry_position_value = self.entry_margin * int(self.cfg.leverage) + self.ding( + f"✅开多:dev={dev * 100:.3f}% size={size} entry={price:.2f} margin={self.entry_margin}u value={self.entry_position_value}u") # ========== 【开单位置2】开空单 ========== # 方向:空(做空,卖出) @@ -684,56 +894,130 @@ class BitmartFuturesMeanReversionBot: self.pos = -1 self.entry_price = price self.entry_ts = time.time() - self.ding(f"✅开空:dev={dev * 100:.3f}% size={size} entry={price:.2f}") + # 记录开仓信息(用于手续费计算) + self.entry_margin = self.cfg.fixed_margin + self.entry_position_value = self.entry_margin * int(self.cfg.leverage) + self.ding( + f"✅开空:dev={dev * 100:.3f}% size={size} entry={price:.2f} margin={self.entry_margin}u value={self.entry_position_value}u") - def maybe_exit(self, price: float, tp: float, sl: float, vol_scale: float): - """检查并执行出场""" + def maybe_exit(self, price: float, ema_value: float, tp: float, sl: float, vol_scale: float): + """ + 检查并执行出场 + 【平仓函数】包含四种平仓条件,所有平仓都会考虑手续费: + 1. 正常平仓:价格回归到EMA附近时平仓(扣除手续费后仍有盈利) + 2. 止盈:达到止盈阈值(扣除手续费后仍有盈利) + 3. 止损:达到止损阈值 + 4. 超时平仓:持仓时间超过限制(扣除手续费后仍有盈利) + """ if self.pos == 0 or self.entry_price is None or self.entry_ts is None: return hold = time.time() - self.entry_ts - if self.pos == 1: - pnl = (price - self.entry_price) / self.entry_price - else: - pnl = (self.entry_price - price) / self.entry_price + # 计算毛盈亏和净盈亏(扣除手续费后) + # calculate_net_pnl已经考虑了杠杆倍数 + net_pnl, total_fee, gross_pnl = self.calculate_net_pnl(price) + # 计算价格偏离EMA的程度 + dev = (price - ema_value) / ema_value if ema_value else 0.0 + + # 计算止损倍数(考虑止损后放宽) sl_mult = 1.0 if self.post_sl_dir == self.pos and self.post_sl_dir != 0: sl_mult = self._post_sl_dynamic_mult() effective_sl = sl * sl_mult - if pnl >= tp: - self.close_position_all() - self.ding(f"🎯止盈:pnl={pnl * 100:.3f}% price={price:.2f} tp={tp * 100:.3f}%") - self.entry_price, self.entry_ts = None, None - self.last_exit_ts = time.time() + # ========== 【平仓条件1】正常平仓:价格回归到EMA附近 ========== + # 这是均值回归策略的核心:当价格回归到EMA附近时,说明均值回归已经完成,应该平仓 + # 但必须扣除手续费后仍有盈利才平仓 + if abs(dev) <= self.cfg.normal_exit_threshold: + if net_pnl > 0: # 扣除手续费后仍有盈利 + if self.close_position_all(): + self.ding( + f"📊正常平仓:价格回归EMA附近 dev={dev * 100:.3f}% " + f"毛盈亏={gross_pnl * 100:.3f}% 净盈亏={net_pnl * 100:.3f}% " + f"手续费={total_fee:.4f}u price={price:.2f}" + ) + self.entry_price, self.entry_ts = None, None + self.entry_margin, self.entry_position_value = None, None + self.last_exit_ts = time.time() + return + else: + logger.error("正常平仓失败,持仓可能仍存在") + # 平仓失败时不更新状态,下次循环会继续尝试 + return - elif pnl <= -effective_sl: + # ========== 【平仓条件2】止盈 ========== + # 达到止盈阈值,且扣除手续费后仍有盈利 + if gross_pnl >= tp: + if net_pnl > 0: # 扣除手续费后仍有盈利 + if self.close_position_all(): + self.ding( + f"🎯止盈:毛盈亏={gross_pnl * 100:.3f}% 净盈亏={net_pnl * 100:.3f}% " + f"手续费={total_fee:.4f}u price={price:.2f} tp={tp * 100:.3f}%" + ) + self.entry_price, self.entry_ts = None, None + self.entry_margin, self.entry_position_value = None, None + self.last_exit_ts = time.time() + return + else: + logger.error("止盈平仓失败,持仓可能仍存在") + return + + # ========== 【平仓条件3】止损 ========== + # 止损使用毛盈亏判断(因为止损是风险控制,即使扣除手续费后亏损也要止损) + if gross_pnl <= -effective_sl: sl_dir = self.pos - self.close_position_all() - self.ding( - f"🛑止损:pnl={pnl * 100:.3f}% price={price:.2f} " - f"sl={sl * 100:.3f}% effective_sl={effective_sl * 100:.3f}%(×{sl_mult:.2f})", - error=True - ) + if self.close_position_all(): + self.ding( + f"🛑止损:毛盈亏={gross_pnl * 100:.3f}% 净盈亏={net_pnl * 100:.3f}% " + f"手续费={total_fee:.4f}u price={price:.2f} " + f"sl={sl * 100:.3f}% effective_sl={effective_sl * 100:.3f}%(×{sl_mult:.2f})", + error=True + ) - self.last_sl_dir = sl_dir - self.last_sl_ts = time.time() + self.last_sl_dir = sl_dir + self.last_sl_ts = time.time() - self.post_sl_dir = sl_dir - self.post_sl_ts = time.time() - self.post_sl_vol_scale = float(vol_scale) + self.post_sl_dir = sl_dir + self.post_sl_ts = time.time() + self.post_sl_vol_scale = float(vol_scale) - self.entry_price, self.entry_ts = None, None - self.last_exit_ts = time.time() + self.entry_price, self.entry_ts = None, None + self.entry_margin, self.entry_position_value = None, None + self.last_exit_ts = time.time() + return + else: + logger.error("止损平仓失败,持仓可能仍存在,风险较高!") + self.ding("⚠️ 止损平仓失败,请手动检查持仓!", error=True) + # 即使平仓失败,也更新止损状态,避免重复触发 + self.last_sl_dir = sl_dir + self.last_sl_ts = time.time() + return - elif hold >= self.cfg.max_hold_sec: - self.close_position_all() - self.ding(f"⏱超时:hold={int(hold)}s pnl={pnl * 100:.3f}% price={price:.2f}") - self.entry_price, self.entry_ts = None, None - self.last_exit_ts = time.time() + # ========== 【平仓条件4】超时平仓 ========== + # 超时平仓也要考虑手续费,只有扣除手续费后仍有盈利才平仓 + if hold >= self.cfg.max_hold_sec: + if net_pnl > 0: # 扣除手续费后仍有盈利 + if self.close_position_all(): + self.ding( + f"⏱超时:hold={int(hold)}s 毛盈亏={gross_pnl * 100:.3f}% " + f"净盈亏={net_pnl * 100:.3f}% 手续费={total_fee:.4f}u price={price:.2f}" + ) + self.entry_price, self.entry_ts = None, None + self.entry_margin, self.entry_position_value = None, None + self.last_exit_ts = time.time() + return + else: + logger.error("超时平仓失败,持仓可能仍存在") + return + else: + # 超时但扣除手续费后亏损,继续持有等待盈利 + logger.debug( + f"⏱超时但净盈亏为负,继续持有:hold={int(hold)}s " + f"毛盈亏={gross_pnl * 100:.3f}% 净盈亏={net_pnl * 100:.3f}%" + ) def notify_status_throttled(self, price: float, ema_value: float, dev: float, bal: float, atr_ratio: float, base_ratio: float, vol_scale: float, @@ -754,6 +1038,13 @@ class BitmartFuturesMeanReversionBot: base_age = int(now - self._base_ratio_ts) if self._base_ratio_ts else -1 + # 计算当前盈亏(如果有持仓) + current_gross_pnl = 0.0 + current_net_pnl = 0.0 + current_fee = 0.0 + if self.pos != 0 and self.entry_price: + current_net_pnl, current_fee, current_gross_pnl = self.calculate_net_pnl(price) + msg = ( f"【BitMart {self.cfg.contract_symbol}|1m均值回归(动态阈值)】\n" f"📊 状态:{direction_str}\n" @@ -762,11 +1053,23 @@ class BitmartFuturesMeanReversionBot: f"🌊 波动率:ATR比={atr_ratio * 100:.3f}% | 基准={base_ratio * 100:.3f}% | 缩放={vol_scale:.2f}\n" f"🎯 动态Floor:入场={entry_floor * 100:.3f}% | 止盈={tp_floor * 100:.3f}% | 止损={sl_floor * 100:.3f}%\n" f"💰 止盈/止损:{tp * 100:.3f}% / {sl * 100:.3f}% (盈亏比:{tp / sl:.2f})\n" + f"📊 正常平仓阈值:±{self.cfg.normal_exit_threshold * 100:.3f}%\n" f"🔄 基准刷新:{self.cfg.base_ratio_refresh_sec}s (已过={base_age}s)\n" f"⚠️ 止损同向加门槛:{'开启' if penalty_active else '关闭'} (方向={self.last_sl_dir})\n" - f"💳 可用余额:{bal:.2f} USDT | 杠杆:{self.cfg.leverage}x\n" - f"⏱️ 持仓限制:{self.cfg.max_hold_sec}s | 冷却:{self.cfg.cooldown_sec_after_exit}s" + f"💳 可用余额:{bal:.2f} USDT | 杠杆:{self.cfg.leverage}x | 固定保证金:{self.cfg.fixed_margin}u\n" ) + + if self.pos != 0 and self.entry_price is not None and self.entry_position_value is not None: + msg += ( + f"📈 当前盈亏:毛盈亏={current_gross_pnl * 100:.3f}% | " + f"净盈亏={current_net_pnl * 100:.3f}% | " + f"手续费={current_fee:.4f}u\n" + f"📍 入场价:{self.entry_price:.2f} | " + f"仓位价值:{self.entry_position_value:.2f}u\n" + ) + + msg += f"⏱️ 持仓限制:{self.cfg.max_hold_sec}s | 冷却:{self.cfg.cooldown_sec_after_exit}s" + self.ding(msg) def openBrowser(self): @@ -914,7 +1217,11 @@ class BitmartFuturesMeanReversionBot: # 7. 检查交易是否启用 if not self.trading_enabled: if self.pos != 0: - self.close_position_all() + if self.close_position_all(): + logger.info("交易被禁用,已平仓") + else: + logger.error("交易被禁用,但平仓失败,请手动检查!") + self.ding("⚠️ 交易被禁用但平仓失败,请手动检查持仓!", error=True) logger.warning("交易被禁用(风控触发),等待...") time.sleep(5) continue @@ -922,12 +1229,14 @@ class BitmartFuturesMeanReversionBot: # 8. 检查危险市场 if self.is_danger_market(klines, price): logger.warning("危险模式:高波动/大实体K,暂停开仓") - self.maybe_exit(price, tp, sl, vol_scale) + self.maybe_exit(price, ema_value, tp, sl, vol_scale) time.sleep(self.cfg.tick_refresh_sec) continue # 9. 执行交易逻辑 - self.maybe_exit(price, tp, sl, vol_scale) + # 先检查平仓(包括正常平仓、止盈、止损) + self.maybe_exit(price, ema_value, tp, sl, vol_scale) + # 再检查开仓 self.maybe_enter(price, ema_value, entry_dev) # 10. 状态通知 @@ -971,3 +1280,4 @@ if __name__ == "__main__": logger.error(f"程序异常退出: {e}") bot.ding(f"❌ 策略异常退出: {e}", error=True) raise + diff --git a/telegram/8619211027341.session b/telegram/8619211027341.session index 3a4596c..66ec9d3 100644 Binary files a/telegram/8619211027341.session and b/telegram/8619211027341.session differ diff --git a/telegram/bot_session.session b/telegram/bot_session.session index 94c7fe7..d90beb1 100644 Binary files a/telegram/bot_session.session and b/telegram/bot_session.session differ diff --git a/telegram/bot_session.session-journal b/telegram/bot_session.session-journal deleted file mode 100644 index ae05151..0000000 Binary files a/telegram/bot_session.session-journal and /dev/null differ diff --git a/telegram/sign.db b/telegram/sign.db index 72bb787..f5dc4f4 100644 Binary files a/telegram/sign.db and b/telegram/sign.db differ