From 3bea1cf69e0d9db69c778b3f50bbfb460f604b44 Mon Sep 17 00:00:00 2001 From: 27942 Date: Mon, 29 Dec 2025 18:29:21 +0800 Subject: [PATCH] fewfef --- bitmart/交易,一直加仓,拉平开仓价位.py | 47 +- bitmart/均线自动化开单.py | 809 +++++++++----------- test1.py | 979 +++++++++++++++++++----- 推特/main.py | 40 +- 4 files changed, 1176 insertions(+), 699 deletions(-) diff --git a/bitmart/交易,一直加仓,拉平开仓价位.py b/bitmart/交易,一直加仓,拉平开仓价位.py index aec9701..7507fab 100644 --- a/bitmart/交易,一直加仓,拉平开仓价位.py +++ b/bitmart/交易,一直加仓,拉平开仓价位.py @@ -5,19 +5,14 @@ BitMart 被动做市/高频刷单策略 """ import time -import datetime -from typing import Optional, Dict, List, Tuple -from dataclasses import dataclass from loguru import logger from threading import Lock -import requests - -from DrissionPage import ChromiumPage, ChromiumOptions +from dataclasses import dataclass from bitmart.api_contract import APIContract -from bitmart.lib.cloud_exceptions import APIException +from typing import Optional, Dict, List, Tuple +from DrissionPage import ChromiumPage, ChromiumOptions - -# from 交易.tools import send_dingtalk_message +from bit_tools import openBrowser # ================================================================ @@ -26,6 +21,7 @@ from bitmart.lib.cloud_exceptions import APIException @dataclass class MarketMakingConfig: + bit_id: str = "f2320f57e24c45529a009e1541e25961" """做市策略配置""" # API配置(仅用于查询,不下单) api_key: str = "a0fb7b98464fd9bcce67e7c519d58ec10d0c38a8" @@ -119,7 +115,8 @@ class PendingOrder: class BrowserManager: """浏览器管理器:负责浏览器的启动、接管和标签页管理""" - def __init__(self, config: MarketMakingConfig): + def __init__(self, config: MarketMakingConfig, bit_id="f2320f57e24c45529a009e1541e25961"): + self.bit_id = "f2320f57e24c45529a009e1541e25961" self.config = config self.tge_port: Optional[int] = None self.page: Optional[ChromiumPage] = None @@ -127,14 +124,10 @@ class BrowserManager: def open_browser(self) -> bool: """打开浏览器并获取端口""" try: - response = requests.post( - f"{self.config.tge_url}/api/browser/start", - json={"envId": self.config.tge_id}, - headers=self.config.tge_headers, - timeout=10 - ) - self.tge_port = response.json()["data"]["port"] - logger.success(f"成功打开浏览器,端口:{self.tge_port}") + bit_port = openBrowser(id=self.bit_id) + co = ChromiumOptions() + co.set_local_port(port=bit_port) + self.page = ChromiumPage(addr_or_opts=co) return True except Exception as e: logger.error(f"打开浏览器失败: {e}") @@ -580,7 +573,8 @@ class BitMartMarketMakerAPI: class MarketMakingStrategy: """被动做市策略(使用浏览器自动化下单,获取高返佣)""" - def __init__(self, config: MarketMakingConfig): + def __init__(self, config: MarketMakingConfig, bit_id=None): + self.bit_id = bit_id self.config = config self.api = BitMartMarketMakerAPI(config) # 仅用于查询 @@ -615,10 +609,10 @@ class MarketMakingStrategy: logger.error("打开浏览器失败") return False - # 接管浏览器 - if not self.browser_manager.take_over_browser(): - logger.error("接管浏览器失败") - return False + # # 接管浏览器 + # if not self.browser_manager.take_over_browser(): + # logger.error("接管浏览器失败") + # return False # 关闭多余标签页 self.browser_manager.close_extra_tabs() @@ -967,7 +961,7 @@ class MarketMakingStrategy: if __name__ == '__main__': config = MarketMakingConfig( contract_symbol="ETHUSDT", - tge_id=196495, # TGE浏览器ID + bit_id="f2320f57e24c45529a009e1541e25961", # TGE浏览器ID trading_url="https://derivatives.bitmart.com/zh-CN/futures/ETHUSDT", spread_percent=0.01, # 0.01%价差 order_size_usdt=0.1, # 每单10 USDT @@ -976,7 +970,7 @@ if __name__ == '__main__': order_timeout=60.0, # 60秒超时 max_daily_loss=50.0, # 每日最大亏损50 USDT max_daily_trades=1000, # 每日最大1000笔 - leverage="30", + leverage="35", open_type="cross" ) @@ -988,6 +982,5 @@ if __name__ == '__main__': logger.error(f"程序异常: {e}") # send_dingtalk_message(f"做市策略异常: {e}", error=True) - # 9359,53 -# 14.35 \ No newline at end of file +# 14.35 diff --git a/bitmart/均线自动化开单.py b/bitmart/均线自动化开单.py index 80e9a8c..3e896f5 100644 --- a/bitmart/均线自动化开单.py +++ b/bitmart/均线自动化开单.py @@ -1,15 +1,12 @@ import time -import uuid import datetime -import requests -from typing import Tuple +from typing import Tuple, Optional, List +from dataclasses import dataclass, field from tqdm import tqdm from loguru import logger -from dataclasses import dataclass from bitmart.api_contract import APIContract from DrissionPage import ChromiumPage, ChromiumOptions -from bitmart.lib.cloud_exceptions import APIException from bit_tools import openBrowser from 交易.tools import send_dingtalk_message @@ -17,11 +14,13 @@ from 交易.tools import send_dingtalk_message @dataclass class StrategyConfig: + """针对90%返佣优化的均值回归策略配置""" + # ============================= - # 1m | ETH 永续 | 控止损≤5/日 + # ETH 永续 | 1分钟K线 | 90%返佣优化 # ============================= - # ===== 合约 ===== + # ===== 合约配置 ===== contract_symbol: str = "ETHUSDT" open_type: str = "cross" leverage: str = "30" @@ -32,52 +31,41 @@ class StrategyConfig: ema_len: int = 36 atr_len: int = 14 - # ========================================================= - # ✅ 自动阈值:ATR/Price 分位数基准(更稳,不被短时噪声带跑) - # ========================================================= + # ===== 动态阈值基准 ===== vol_baseline_window: int = 60 vol_baseline_quantile: float = 0.65 vol_scale_min: float = 0.80 vol_scale_max: float = 1.60 - - # ✅ baseline 每 60 秒刷新一次(体感更明显、也省CPU) base_ratio_refresh_sec: int = 60 - # ========================================================= - # ✅ 动态 floor(方案一) - # floor = clamp(min, base_k * base_ratio, max) - # 目的:跟着典型波动变,过滤小噪声;tp/sl 也随环境自适应 - # ========================================================= - # entry_dev_floor 动态 - entry_dev_floor_min: float = 0.0012 # 0.12% - entry_dev_floor_max: float = 0.0030 # 0.30%(可按你偏好调) - entry_dev_floor_base_k: float = 1.10 # entry_floor = 1.10 * base_ratio + # ===== 动态floor配置(针对低手续费优化) ===== + entry_dev_floor_min: float = 0.0008 # 0.08%(原0.12%) + entry_dev_floor_max: float = 0.0020 # 0.20%(原0.30%) + entry_dev_floor_base_k: float = 1.10 - # tp_floor 动态 - tp_floor_min: float = 0.0006 # 0.06% - tp_floor_max: float = 0.0020 # 0.20% - tp_floor_base_k: float = 0.55 # tp_floor = 0.55 * base_ratio(止盈别太大,1m回归更实际) + tp_floor_min: float = 0.0004 # 0.04%(原0.06%) + tp_floor_max: float = 0.0015 # 0.15%(原0.20%) + tp_floor_base_k: float = 0.55 - # sl_floor 动态 - sl_floor_min: float = 0.0018 # 0.18% - sl_floor_max: float = 0.0060 # 0.60% - sl_floor_base_k: float = 1.35 # sl_floor = 1.35 * base_ratio(ETH 1m 插针多,止损下限可更稳) + sl_floor_min: float = 0.0012 # 0.12%(原0.18%) + sl_floor_max: float = 0.0040 # 0.40%(原0.60%) + sl_floor_base_k: float = 1.35 - # ========================================================= - # ✅ 动态阈值倍率(仍然保留你原来思路) - # ========================================================= - entry_k: float = 1.45 - tp_k: float = 0.65 - sl_k: float = 1.05 + # ===== 阈值倍率(针对低手续费优化) ===== + entry_k: float = 1.30 # 原1.45 + tp_k: float = 0.55 # 原0.65 + sl_k: float = 0.90 # 原1.05 - # ===== 时间/冷却 ===== - max_hold_sec: int = 75 - cooldown_sec_after_exit: int = 20 + # ===== 时间/冷却(降低以增加频率) ===== + max_hold_sec: int = 60 # 原75秒 + cooldown_sec_after_exit: int = 5 # 原20秒 + opposite_direction_cooldown: int = 30 # 平仓后同向冷却30秒 - # ===== 下单/仓位 ===== - risk_percent: float = 0.004 + # ===== 仓位管理 ===== + fixed_margin: float = 10.0 # 固定保证金 min_size: int = 1 - max_size: int = 5000 + max_size: int = 10 + max_daily_trades: int = 50 # 每日最大交易次数 # ===== 日内风控 ===== daily_loss_limit: float = 0.02 @@ -92,43 +80,44 @@ class StrategyConfig: tick_refresh_sec: int = 1 status_notify_sec: int = 60 - # ========================================================= - # ✅ 止损后同向入场加门槛(但不禁止同向重入) - # ========================================================= + # ===== 止损后机制 ===== reentry_penalty_mult: float = 1.55 reentry_penalty_max_sec: int = 180 reset_band_k: float = 0.45 reset_band_floor: float = 0.0006 - # ========================================================= - # ✅ 止损后同方向 SL 放宽幅度与"止损时 vol_scale"联动 - # ========================================================= post_sl_sl_max_sec: int = 90 post_sl_mult_min: float = 1.02 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%,正常平仓最小盈利要求 + # ===== 正常平仓条件(针对低手续费优化) ===== + normal_exit_threshold: float = 0.0002 # 0.02%(原0.03%) + normal_exit_min_profit: float = 0.0001 # 0.01%(原0.02%) - # ========================================================= - # ✅ 手续费配置(固定10u,30倍杠杆) - # ========================================================= - fixed_margin: float = 10.0 # 固定每单10u保证金 + # ===== 90%返佣配置 ===== platform_fee_rate: float = 0.0005 # 平台手续费:开仓价值的万分之五 rebate_rate: float = 0.90 # 返佣比例:90% + # ===== 新增:趋势过滤 ===== + use_trend_filter: bool = True + trend_ema_len: int = 50 + trend_threshold: float = 0.0005 # 0.05% + + # ===== 新增:成交量确认 ===== + require_volume_confirmation: bool = True + volume_ma_len: int = 20 + volume_threshold: float = 1.2 + + # ===== 新增:连续亏损控制 ===== + max_consecutive_losses: int = 3 + recovery_wait_sec: int = 60 + class BitmartFuturesMeanReversionBot: def __init__(self, cfg: StrategyConfig, bit_id=None): - self.bit_id = bit_id - - self.page: ChromiumPage | None = None - + self.page: Optional[ChromiumPage] = None self.cfg = cfg self.api_key = "a0fb7b98464fd9bcce67e7c519d58ec10d0c38a8" @@ -136,49 +125,54 @@ class BitmartFuturesMeanReversionBot: self.memo = "合约交易" if not self.api_key or not self.secret_key: - raise RuntimeError("请先设置环境变量 BITMART_API_KEY / BITMART_SECRET_KEY / BITMART_MEMO(可选)") + raise RuntimeError("请先设置API密钥") self.contractAPI = APIContract(self.api_key, self.secret_key, self.memo, timeout=(5, 15)) - # 持仓状态: -1 空, 0 无, 1 多 - self.pos = 0 - self.entry_price = None - self.entry_ts = None - self.last_exit_ts = 0 + # 持仓状态 + self.pos = 0 # -1 空, 0 无, 1 多 + self.entry_price: Optional[float] = None + self.entry_ts: Optional[float] = None + self.last_exit_ts = 0.0 - # 开仓信息(用于手续费计算) - self.entry_margin = None # 开仓保证金(固定10u) - self.entry_position_value = None # 开仓时的仓位价值(保证金 * 杠杆) + # 新增:记录上次平仓方向 + self.last_exit_direction = 0 # 1=多平, -1=空平 + self.last_exit_price = 0.0 + + # 开仓信息 + self.entry_margin: Optional[float] = None + self.entry_position_value: Optional[float] = None # 日内权益基准 - self.day_start_equity = None + self.day_start_equity: Optional[float] = None self.trading_enabled = True self.day_tag = datetime.date.today() + self.today_trade_count = 0 + self.consecutive_losses = 0 # 缓存 - self._klines_cache = None - self._klines_cache_ts = 0 - self._last_status_notify_ts = 0 + self._klines_cache: Optional[List] = None + self._klines_cache_ts = 0.0 + self._last_status_notify_ts = 0.0 - # ✅ base_ratio 缓存 - self._base_ratio_cached = 0.0015 # 初始化默认值 0.15% + # 基准波动率缓存 + self._base_ratio_cached = 0.0015 self._base_ratio_ts = 0.0 - # ✅ 止损后"同向入场加门槛"状态 - self.last_sl_dir = 0 # 1=多止损,-1=空止损,0=无 + # 止损后机制状态 + self.last_sl_dir = 0 self.last_sl_ts = 0.0 - - # ✅ 止损后"同方向 SL 联动放宽"状态 self.post_sl_dir = 0 self.post_sl_ts = 0.0 - self.post_sl_vol_scale = 1.0 # 记录止损时的 vol_scale + self.post_sl_vol_scale = 1.0 self.pbar = tqdm(total=60, desc="运行中(秒)", ncols=90) - logger.info(f"初始化完成,基准波动率默认值: {self._base_ratio_cached * 100:.4f}%") + logger.info(f"初始化完成 - 合约:{cfg.contract_symbol}, 杠杆:{cfg.leverage}x, 返佣:{cfg.rebate_rate * 100}%") - # ----------------- 通用工具 ----------------- - def ding(self, msg, error=False): + # ----------------- 工具函数 ----------------- + def ding(self, msg: str, error: bool = False): + """发送钉钉通知""" prefix = "❌bitmart:" if error else "🔔bitmart:" if error: for _ in range(1): @@ -187,6 +181,7 @@ class BitmartFuturesMeanReversionBot: send_dingtalk_message(f"{prefix}{msg}") def set_leverage(self) -> bool: + """设置杠杆""" try: resp = self.contractAPI.post_submit_leverage( contract_symbol=self.cfg.contract_symbol, @@ -197,15 +192,14 @@ class BitmartFuturesMeanReversionBot: logger.success(f"设置杠杆成功:{self.cfg.open_type} + {self.cfg.leverage}x") return True logger.error(f"设置杠杆失败: {resp}") - self.ding(f"设置杠杆失败: {resp}", error=True) return False except Exception as e: logger.error(f"设置杠杆异常: {e}") - self.ding(f"设置杠杆异常: {e}", error=True) return False - # ----------------- 行情/指标 ----------------- - def get_klines_cached(self): + # ----------------- 行情数据 ----------------- + def get_klines_cached(self) -> Optional[List]: + """获取缓存的K线数据""" now = time.time() if self._klines_cache is not None and (now - self._klines_cache_ts) < self.cfg.klines_refresh_sec: return self._klines_cache @@ -216,7 +210,8 @@ class BitmartFuturesMeanReversionBot: self._klines_cache_ts = now return self._klines_cache - def get_klines(self): + def get_klines(self) -> Optional[List]: + """获取K线数据""" try: end_time = int(time.time()) start_time = end_time - 60 * self.cfg.lookback_min @@ -246,10 +241,10 @@ class BitmartFuturesMeanReversionBot: return formatted except Exception as e: logger.error(f"获取K线异常: {e}") - self.ding(f"获取K线异常: {e}", error=True) return None def get_last_price(self, fallback_close: float) -> float: + """获取最新价格""" try: if hasattr(self.contractAPI, "get_contract_details"): r = self.contractAPI.get_contract_details(contract_symbol=self.cfg.contract_symbol)[0] @@ -272,7 +267,8 @@ class BitmartFuturesMeanReversionBot: return float(fallback_close) @staticmethod - def ema(values, n: int) -> float: + def ema(values: List[float], n: int) -> float: + """计算EMA""" k = 2 / (n + 1) e = values[0] for v in values[1:]: @@ -280,7 +276,8 @@ class BitmartFuturesMeanReversionBot: return e @staticmethod - def atr(klines, n: int) -> float: + def atr(klines: List[dict], n: int) -> float: + """计算ATR""" if len(klines) < n + 1: return 0.0 trs = [] @@ -295,35 +292,61 @@ class BitmartFuturesMeanReversionBot: trs.append(tr) return sum(trs) / len(trs) - def is_danger_market(self, klines, price: float) -> bool: + # ----------------- 风控检查 ----------------- + def is_danger_market(self, klines: List[dict], price: float) -> bool: + """检查是否危险市场""" last = klines[-1] body = abs(last["close"] - last["open"]) / last["open"] if last["open"] else 0.0 if body >= self.cfg.big_body_kill: + logger.warning(f"大实体K线: {body * 100:.2f}%") return True a = self.atr(klines, self.cfg.atr_len) atr_ratio = (a / price) if price > 0 else 0.0 if atr_ratio >= self.cfg.atr_ratio_kill: + logger.warning(f"高ATR比率: {atr_ratio * 100:.2f}%") return True return False - def atr_ratio_baseline(self, klines) -> float: - """简化版ATR基准计算""" + def check_trend_filter(self, price: float) -> bool: + """趋势过滤器""" + if not self.cfg.use_trend_filter: + return True + + klines = self.get_klines_cached() + if not klines or len(klines) < self.cfg.trend_ema_len: + return True + + try: + closes = [k["close"] for k in klines[-(self.cfg.trend_ema_len + 1):]] + trend_ema = self.ema(closes, self.cfg.trend_ema_len) + trend_dev = (price - trend_ema) / trend_ema if trend_ema else 0.0 + + if abs(trend_dev) > self.cfg.trend_threshold: + logger.debug(f"趋势过滤触发: 偏差={trend_dev * 100:.3f}%, 阈值={self.cfg.trend_threshold * 100:.3f}%") + return False + + return True + except Exception as e: + logger.error(f"趋势过滤异常: {e}") + return True + + # ----------------- 波动率计算 ----------------- + def atr_ratio_baseline(self, klines: List[dict]) -> float: + """计算ATR比率基准""" window = min(self.cfg.vol_baseline_window, len(klines) - self.cfg.atr_len - 1) - if window <= 10: # 数据太少 + if window <= 10: logger.warning(f"数据不足计算基准: {len(klines)}根K线") return 0.0 ratios = [] - - # 简化计算:每隔3根K线计算一个ATR比率(减少计算量) step = 3 + for i in range(-window, 0, step): if len(klines) + i < self.cfg.atr_len + 1: continue - # 计算当前位置的ATR start_idx = len(klines) + i - self.cfg.atr_len end_idx = len(klines) + i @@ -331,20 +354,16 @@ class BitmartFuturesMeanReversionBot: continue sub_klines = klines[start_idx:end_idx] - - # 确保有足够数据计算ATR if len(sub_klines) >= self.cfg.atr_len + 1: a = self.atr(sub_klines, self.cfg.atr_len) price = klines[end_idx - 1]["close"] - if a > 0 and price > 0: ratio = a / price - if 0.0001 < ratio < 0.01: # 过滤异常值 + if 0.0001 < ratio < 0.01: ratios.append(ratio) - if len(ratios) < 5: # 样本太少 - # 尝试直接使用整个数据计算一个ATR比率 - a = self.atr(klines[-60:], self.cfg.atr_len) # 使用最近60根K线 + if len(ratios) < 5: + a = self.atr(klines[-60:], self.cfg.atr_len) price = klines[-1]["close"] if a > 0 and price > 0: baseline = a / price @@ -353,51 +372,44 @@ class BitmartFuturesMeanReversionBot: else: return 0.0 - # 计算分位数 ratios.sort() idx = min(len(ratios) - 1, max(0, int(self.cfg.vol_baseline_quantile * (len(ratios) - 1)))) baseline = ratios[idx] - logger.debug(f"基准计算: 样本数={len(ratios)}, 基准={baseline * 100:.4f}%, " - f"范围=[{ratios[0] * 100:.4f}%, {ratios[-1] * 100:.4f}%]") - + logger.debug(f"基准计算: 样本数={len(ratios)}, 基准={baseline * 100:.4f}%") return baseline - def get_base_ratio_cached(self, klines) -> float: - """获取缓存的基准波动率,定期刷新""" + def get_base_ratio_cached(self, klines: List[dict]) -> float: + """获取缓存的基准波动率""" now = time.time() refresh_sec = self.cfg.base_ratio_refresh_sec if (self._base_ratio_cached is None or (now - self._base_ratio_ts) >= refresh_sec): - # 使用简单版本的基准计算 baseline = self.atr_ratio_baseline(klines) - if baseline > 0.0001: # 大于0.01%才认为是有效值 + if baseline > 0.0001: self._base_ratio_cached = baseline self._base_ratio_ts = now logger.info(f"基准波动率更新: {baseline * 100:.4f}%") else: - # 使用基于价格的动态默认值 current_price = klines[-1]["close"] if klines else 3000 - # ETH价格越高,基准波动率越小(百分比) if current_price > 4000: - default_baseline = 0.0010 # 0.10% + default_baseline = 0.0010 elif current_price > 3500: - default_baseline = 0.0012 # 0.12% + default_baseline = 0.0012 elif current_price > 3000: - default_baseline = 0.0015 # 0.15% + default_baseline = 0.0015 elif current_price > 2500: - default_baseline = 0.0018 # 0.18% + default_baseline = 0.0018 else: - default_baseline = 0.0020 # 0.20% + default_baseline = 0.0020 self._base_ratio_cached = default_baseline self._base_ratio_ts = now - logger.warning(f"使用价格动态默认基准: {default_baseline * 100:.4f}% " - f"(价格=${current_price:.0f})") + logger.warning(f"使用价格动态默认基准: {default_baseline * 100:.4f}%") return self._base_ratio_cached @@ -406,35 +418,26 @@ class BitmartFuturesMeanReversionBot: """限制数值在指定范围内""" return max(lo, min(hi, x)) - def dynamic_thresholds(self, atr_ratio: float, base_ratio: float): - """ - ✅ entry/tp/sl 全部动态(修复版): - - vol_scale:atr_ratio/base_ratio 限幅 - - floor:方案一 (floor = clamp(min, k*base_ratio, max)) - - 最终阈值:max(floor, k * vol_scale * atr_ratio) - """ - # 1) 检查输入有效性 + # ----------------- 动态阈值计算 ----------------- + def dynamic_thresholds(self, atr_ratio: float, base_ratio: float) -> Tuple[ + float, float, float, float, float, float, float]: + """计算动态阈值""" if atr_ratio <= 0: logger.warning(f"ATR比率异常: {atr_ratio}") - atr_ratio = 0.001 # 默认值 0.1% + atr_ratio = 0.001 - # 2) 如果base_ratio太小或无效,使用调整后的atr_ratio - if base_ratio < 0.0005: # 小于0.05%视为无效 - base_ratio = max(0.001, atr_ratio * 1.2) # 比当前ATR比率稍大 + if base_ratio < 0.0005: + base_ratio = max(0.001, atr_ratio * 1.2) logger.debug(f"基准太小,使用调整后的atr_ratio: {base_ratio * 100:.4f}%") - # 3) vol_scale计算 + # vol_scale计算 if base_ratio > 0: raw_scale = atr_ratio / base_ratio vol_scale = self._clamp(raw_scale, self.cfg.vol_scale_min, self.cfg.vol_scale_max) - logger.debug( - f"vol_scale: {raw_scale:.2f} → {vol_scale:.2f} (atr={atr_ratio * 100:.3f}%, base={base_ratio * 100:.3f}%)") else: vol_scale = 1.0 - logger.warning(f"基准无效,使用默认vol_scale=1.0") - # 4) 动态floor计算 - # Entry floor + # 动态floor计算 entry_floor_raw = self.cfg.entry_dev_floor_base_k * base_ratio entry_floor = self._clamp( entry_floor_raw, @@ -442,7 +445,6 @@ class BitmartFuturesMeanReversionBot: self.cfg.entry_dev_floor_max, ) - # TP floor tp_floor_raw = self.cfg.tp_floor_base_k * base_ratio tp_floor = self._clamp( tp_floor_raw, @@ -450,7 +452,6 @@ class BitmartFuturesMeanReversionBot: self.cfg.tp_floor_max, ) - # SL floor sl_floor_raw = self.cfg.sl_floor_base_k * base_ratio sl_floor = self._clamp( sl_floor_raw, @@ -458,7 +459,7 @@ class BitmartFuturesMeanReversionBot: self.cfg.sl_floor_max, ) - # 5) 最终阈值计算 + # 最终阈值 entry_dev_atr_part = self.cfg.entry_k * vol_scale * atr_ratio entry_dev = max(entry_floor, entry_dev_atr_part) @@ -468,20 +469,18 @@ class BitmartFuturesMeanReversionBot: sl_atr_part = self.cfg.sl_k * vol_scale * atr_ratio sl = max(sl_floor, sl_atr_part) - # 6) 确保entry_dev不会太小 entry_dev = max(entry_dev, self.cfg.entry_dev_floor_min) - # 7) 输出详细信息 logger.info( - f"动态阈值: atr={atr_ratio * 100:.4f}%, base={base_ratio * 100:.4f}%, " - f"vol_scale={vol_scale:.2f}, floor={entry_floor * 100:.4f}%, " - f"atr_part={entry_dev_atr_part * 100:.4f}%, 最终entry_dev={entry_dev * 100:.4f}%" + f"动态阈值: entry={entry_dev * 100:.4f}%, tp={tp * 100:.4f}%, sl={sl * 100:.4f}%, " + f"vol_scale={vol_scale:.2f}" ) return entry_dev, tp, sl, vol_scale, entry_floor, tp_floor, sl_floor - # ----------------- 账户/仓位 ----------------- + # ----------------- 账户仓位管理 ----------------- def get_assets_available(self) -> float: + """获取可用余额""" try: resp = self.contractAPI.get_assets_detail()[0] if resp.get("code") != 1000: @@ -499,6 +498,7 @@ class BitmartFuturesMeanReversionBot: return 0.0 def get_position_status(self) -> bool: + """获取持仓状态""" try: resp = self.contractAPI.get_position(contract_symbol=self.cfg.contract_symbol)[0] if resp.get("code") != 1000: @@ -514,21 +514,25 @@ class BitmartFuturesMeanReversionBot: return True except Exception as e: logger.error(f"持仓查询异常: {e}") - self.ding(f"持仓查询异常: {e}", error=True) return False def get_equity_proxy(self) -> float: + """获取权益""" return self.get_assets_available() def refresh_daily_baseline(self): + """刷新日内基准""" today = datetime.date.today() if today != self.day_tag: self.day_tag = today self.day_start_equity = None self.trading_enabled = True - self.ding(f"新的一天({today}):重置日内风控基准") + self.today_trade_count = 0 + self.consecutive_losses = 0 + logger.info(f"新的一天({today}):重置日内风控基准") def risk_kill_switch(self): + """风险控制开关""" self.refresh_daily_baseline() equity = self.get_equity_proxy() if equity <= 0: @@ -548,46 +552,63 @@ class BitmartFuturesMeanReversionBot: self.trading_enabled = False self.ding(f"达到日盈利封顶:{pnl * 100:.2f}% -> 停机") - # ----------------- 下单 ----------------- def calculate_size(self, price: float) -> int: - """ - 计算仓位大小 - 固定每单10u保证金,30倍杠杆 - """ + """计算仓位大小""" bal = self.get_assets_available() if bal < self.cfg.fixed_margin: logger.warning(f"余额不足:{bal:.2f} USDT < {self.cfg.fixed_margin} USDT") return 0 - # 固定保证金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}") + logger.debug(f"计算仓位:保证金={margin}u, 杠杆={lev}x, size={size}") return size - def place_market_order(self, side: int, size: int) -> bool: - """ - 【下单函数】实际执行下单操作 - side: 1=开多, 4=开空, 2=平多, 3=平空 - """ - if size <= 0: + # ----------------- 浏览器操作 ----------------- + def openBrowser(self) -> bool: + """打开浏览器""" + try: + bit_port = openBrowser(id=self.bit_id) + co = ChromiumOptions() + co.set_local_port(port=bit_port) + self.page = ChromiumPage(addr_or_opts=co) + return True + except Exception as e: + logger.error(f"打开浏览器失败: {e}") return False - size = 100 - + def click_safe(self, xpath: str, sleep: float = 0.5) -> bool: + """安全点击""" try: + ele = self.page.ele(xpath) + if not ele: + return False + ele.scroll.to_see(center=True) + time.sleep(sleep) + ele.click() + return True + except Exception as e: + logger.error(f"点击失败: {e}") + return False + + # ----------------- 订单执行 ----------------- + def place_market_order(self, side: int, size: int) -> bool: + """下市价单""" + if size <= 0: + return False + size = 10 + try: + # 确保size在合理范围内 + size = max(self.cfg.min_size, min(self.cfg.max_size, size)) + # 开多单 if side == 1: self.click_safe('x://button[normalize-space(text()) ="市价"]') - self.page.ele('x://*[@id="size_0"]').input(size, clear=True) + self.page.ele('x://*[@id="size_0"]').input(str(size), clear=True) self.click_safe('x://span[normalize-space(text()) ="买入/做多"]') logger.info(f"✅ 开多单: size={size}") return True @@ -595,30 +616,29 @@ class BitmartFuturesMeanReversionBot: # 开空单 elif side == 4: self.click_safe('x://button[normalize-space(text()) ="市价"]') - self.page.ele('x://*[@id="size_0"]').input(size, clear=True) + self.page.ele('x://*[@id="size_0"]').input(str(size), clear=True) self.click_safe('x://span[normalize-space(text()) ="卖出/做空"]') logger.info(f"✅ 开空单: size={size}") return True - # 平多单(平多 = 卖出) + # 平多单 elif side == 2: self.click_safe('x://span[normalize-space(text()) ="市价"]') - time.sleep(0.3) # 等待界面响应 - # 平仓时size可以设置大一些确保全部平仓 - self.page.ele('x://*[@id="size_0"]').input(size, clear=True) + time.sleep(0.3) + self.page.ele('x://*[@id="size_0"]').input(str(size), clear=True) time.sleep(0.3) self.click_safe('x://span[normalize-space(text()) ="卖出/做空"]') - logger.info(f"✅ 平多单操作执行: size={size}") + logger.info(f"✅ 平多单: size={size}") return True - # 平空单(平空 = 买入) + # 平空单 elif side == 3: self.click_safe('x://span[normalize-space(text()) ="市价"]') - time.sleep(0.3) # 等待界面响应 - self.page.ele('x://*[@id="size_0"]').input(size, clear=True) + time.sleep(0.3) + self.page.ele('x://*[@id="size_0"]').input(str(size), clear=True) time.sleep(0.3) self.click_safe('x://span[normalize-space(text()) ="买入/做多"]') - logger.info(f"✅ 平空单操作执行: size={size}") + logger.info(f"✅ 平空单: size={size}") return True else: @@ -627,27 +647,16 @@ class BitmartFuturesMeanReversionBot: except Exception as e: logger.error(f"下单异常: {e}") - self.ding(f"下单异常: {e}", error=True) return False def calculate_fee(self, position_value: float) -> float: - """ - 计算手续费 - 平台手续费 = 仓位价值 * 0.0005(万分之五) - 实际手续费 = 平台手续费 * (1 - 返佣比例) = 平台手续费 * 0.1 - """ + """计算手续费(考虑90%返佣)""" 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 @@ -662,15 +671,10 @@ class BitmartFuturesMeanReversionBot: # 计算毛盈亏比例(考虑杠杆) 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 # 手续费相对于保证金的比率 @@ -682,27 +686,20 @@ class BitmartFuturesMeanReversionBot: 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秒 + retry_delay = 1.0 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})") @@ -713,7 +710,6 @@ class BitmartFuturesMeanReversionBot: 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})") @@ -724,43 +720,31 @@ class BitmartFuturesMeanReversionBot: self.ding(f"平空单失败:已重试{max_retries}次仍失败", error=True) return False else: - logger.info("持仓状态异常,无需平仓") return True # 等待订单执行 - time.sleep(1.5) # 等待订单执行 + 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})") + 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) + 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: @@ -768,32 +752,22 @@ class BitmartFuturesMeanReversionBot: 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 '空'})") + logger.warning(f"⚠️ SDK验证:持仓仍存在") return False - # 持仓已改变或不存在(平仓成功) logger.info("✅ SDK验证:持仓已平仓或已改变") return True except Exception as e: logger.error(f"验证持仓状态异常: {e}") - # 验证失败时,保守处理:认为可能未平仓 return False # ----------------- 止损后机制 ----------------- @@ -824,199 +798,197 @@ class BitmartFuturesMeanReversionBot: return 1.0 raw = 1.0 + self.cfg.post_sl_vol_alpha * (self.post_sl_vol_scale - 1.0) - raw = max(1.0, raw) # 不缩小止损,只放宽 + raw = max(1.0, raw) return max(self.cfg.post_sl_mult_min, min(self.cfg.post_sl_mult_max, raw)) - # ----------------- 交易逻辑 ----------------- + # ----------------- 交易逻辑(核心修复) ----------------- def in_cooldown(self) -> bool: """检查是否在冷却期内""" return (time.time() - self.last_exit_ts) < self.cfg.cooldown_sec_after_exit def maybe_enter(self, price: float, ema_value: float, entry_dev: float): """ - 检查并执行入场 - 【开单入口函数】此函数负责判断是否开单以及开单方向 - - 开多:价格低于EMA超过阈值时 - - 开空:价格高于EMA超过阈值时 + 检查并执行入场 - 修复版 + 修复问题:避免平仓后立即开反向仓位 """ + # 基础检查 if self.pos != 0: return + if self.in_cooldown(): return + if not self.trading_enabled: + return + + # 连续亏损控制 + if self.consecutive_losses >= self.cfg.max_consecutive_losses: + logger.warning(f"连续亏损{self.consecutive_losses}次,暂停交易{self.cfg.recovery_wait_sec}秒") + time.sleep(self.cfg.recovery_wait_sec) + return + + # 日内交易次数限制 + if self.today_trade_count >= self.cfg.max_daily_trades: + logger.info(f"达到日交易次数上限: {self.today_trade_count}") + return + + # 趋势过滤 + if not self.check_trend_filter(price): + return + dev = (price - ema_value) / ema_value if ema_value else 0.0 size = self.calculate_size(price) if size <= 0: return - penalty_active = self._reentry_penalty_active(dev, entry_dev) + # 方向过滤:避免平仓后立即反向开仓 + if self.last_exit_direction != 0: + time_since_exit = time.time() - self.last_exit_ts + # 如果是平多后(做空方向),且在冷却期内,避免立即开多 + if (self.last_exit_direction == 1 and # 上次是多平 + time_since_exit < self.cfg.opposite_direction_cooldown and + dev <= 0): # 当前适合开多 + logger.info(f"平多后{int(time_since_exit)}秒内,避免开多") + return + + # 如果是平空后(做多方向),且在冷却期内,避免立即开空 + if (self.last_exit_direction == -1 and # 上次是空平 + time_since_exit < self.cfg.opposite_direction_cooldown and + dev >= 0): # 当前适合开空 + logger.info(f"平空后{int(time_since_exit)}秒内,避免开空") + return + + penalty_active = self._reentry_penalty_active(dev, entry_dev) long_th = -entry_dev short_th = entry_dev if penalty_active: if self.last_sl_dir == 1: long_th = -entry_dev * self.cfg.reentry_penalty_mult - logger.info( - f"多头止损后惩罚生效: 入场阈值从 {long_th * 100:.3f}% 调整为 {(-entry_dev * self.cfg.reentry_penalty_mult) * 100:.3f}%") elif self.last_sl_dir == -1: short_th = entry_dev * self.cfg.reentry_penalty_mult - logger.info( - f"空头止损后惩罚生效: 入场阈值从 {short_th * 100:.3f}% 调整为 {(entry_dev * self.cfg.reentry_penalty_mult) * 100:.3f}%") logger.info( - f"入场检查: price={price:.2f}, ema={ema_value:.2f}, dev={dev * 100:.3f}% " - f"(entry_dev={entry_dev * 100:.3f}%, long_th={long_th * 100:.3f}%, short_th={short_th * 100:.3f}%) " - f"size={size}, penalty={penalty_active}, last_sl_dir={self.last_sl_dir}" + f"入场检查: 偏离={dev * 100:.3f}%, 阈值=[{long_th * 100:.3f}%, {short_th * 100:.3f}%], " + f"size={size}, 连续亏损={self.consecutive_losses}" ) - # ========== 【开单位置1】开多单 ========== - # 方向:多(做多,买入) - # 条件:价格偏离EMA向下超过阈值 (dev <= long_th) + # 开多条件:价格显著低于EMA if dev <= long_th: - if self.place_market_order(1, size): # side=1 表示开多 + if self.place_market_order(1, size): self.pos = 1 self.entry_price = price self.entry_ts = time.time() - # 记录开仓信息(用于手续费计算) 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") + self.ding(f"✅开多:偏离={dev * 100:.3f}%, 价格={price:.2f}") - # ========== 【开单位置2】开空单 ========== - # 方向:空(做空,卖出) - # 条件:价格偏离EMA向上超过阈值 (dev >= short_th) + # 开空条件:价格显著高于EMA elif dev >= short_th: - if self.place_market_order(4, size): # side=4 表示开空 + if self.place_market_order(4, size): self.pos = -1 self.entry_price = price self.entry_ts = time.time() - # 记录开仓信息(用于手续费计算) 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") + self.ding(f"✅开空:偏离={dev * 100:.3f}%, 价格={price:.2f}") 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 - - # 计算毛盈亏和净盈亏(扣除手续费后) - # 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 - # ========== 【平仓条件1】正常平仓:价格回归到EMA附近 ========== - # 这是均值回归策略的核心:当价格回归到EMA附近时,说明均值回归已经完成,应该平仓 - # 但必须扣除手续费后仍有盈利才平仓 + # 记录平仓前的方向 + old_pos = self.pos + + # 条件1:正常平仓(价格回归EMA) if abs(dev) <= self.cfg.normal_exit_threshold: - if net_pnl > 0: # 扣除手续费后仍有盈利 + if net_pnl >= self.cfg.normal_exit_min_profit: # 净盈利达到最小要求 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() + self._record_exit(price, old_pos, net_pnl, total_fee, gross_pnl, "正常平仓") return else: - logger.error("正常平仓失败,持仓可能仍存在") - # 平仓失败时不更新状态,下次循环会继续尝试 + logger.error("正常平仓失败") return - # ========== 【平仓条件2】止盈 ========== - # 达到止盈阈值,且扣除手续费后仍有盈利 + # 条件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() + self._record_exit(price, old_pos, net_pnl, total_fee, gross_pnl, "止盈") return else: - logger.error("止盈平仓失败,持仓可能仍存在") + logger.error("止盈平仓失败") return - # ========== 【平仓条件3】止损 ========== - # 止损使用毛盈亏判断(因为止损是风险控制,即使扣除手续费后亏损也要止损) + # 条件3:止损 if gross_pnl <= -effective_sl: - sl_dir = self.pos - 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._record_exit(price, old_pos, net_pnl, total_fee, gross_pnl, "止损") + # 记录止损状态 + sl_dir = old_pos 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.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() + logger.error("止损平仓失败") return - # ========== 【平仓条件4】超时平仓 ========== - # 超时平仓也要考虑手续费,只有扣除手续费后仍有盈利才平仓 + # 条件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() + self._record_exit(price, old_pos, net_pnl, total_fee, gross_pnl, "超时平仓") return else: - logger.error("超时平仓失败,持仓可能仍存在") + logger.error("超时平仓失败") return else: - # 超时但扣除手续费后亏损,继续持有等待盈利 - logger.debug( - f"⏱超时但净盈亏为负,继续持有:hold={int(hold)}s " - f"毛盈亏={gross_pnl * 100:.3f}% 净盈亏={net_pnl * 100:.3f}%" - ) + logger.debug(f"超时但净盈亏为负,继续持有") + def _record_exit(self, price: float, old_pos: int, net_pnl: float, + total_fee: float, gross_pnl: float, exit_type: str): + """记录平仓信息""" + self.entry_price = None + self.entry_ts = None + self.entry_margin = None + self.entry_position_value = None + self.last_exit_ts = time.time() + self.last_exit_direction = old_pos + self.last_exit_price = price + self.today_trade_count += 1 + + # 更新连续亏损计数 + if net_pnl < 0: + self.consecutive_losses += 1 + else: + self.consecutive_losses = 0 + + self.ding( + f"📊{exit_type}:方向={'多' if old_pos == 1 else '空'}, " + f"毛盈亏={gross_pnl * 100:.3f}%, 净盈亏={net_pnl * 100:.3f}%, " + f"手续费={total_fee:.4f}u, 价格={price:.2f}" + ) + + # ----------------- 状态通知 ----------------- def notify_status_throttled(self, price: float, ema_value: float, dev: float, bal: float, atr_ratio: float, base_ratio: float, vol_scale: float, entry_dev: float, tp: float, sl: float, @@ -1028,139 +1000,48 @@ class BitmartFuturesMeanReversionBot: self._last_status_notify_ts = now direction_str = "多" if self.pos == 1 else ("空" if self.pos == -1 else "无") - penalty_active = self._reentry_penalty_active(dev, entry_dev) - sl_mult = 1.0 - if self.pos != 0 and self.post_sl_dir == self.pos: - sl_mult = self._post_sl_dynamic_mult() - - 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 + # 计算当前盈亏 + current_net_pnl, current_fee, current_gross_pnl = 0.0, 0.0, 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" - f"💰 现价:{price:.2f} | EMA{self.cfg.ema_len}:{ema_value:.2f}\n" - f"📈 偏离:{dev * 100:.3f}% (入场阈值:±{entry_dev * 100:.3f}%)\n" - 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 | 固定保证金:{self.cfg.fixed_margin}u\n" + f"【BitMart {self.cfg.contract_symbol}|1m均值回归(90%返佣优化)】\n" + f"📊 状态:{direction_str} | 今日交易:{self.today_trade_count}/{self.cfg.max_daily_trades}\n" + f"💰 现价:{price:.2f} | EMA{self.cfg.ema_len}:{ema_value:.2f} | 偏离:{dev * 100:.3f}%\n" + f"🎯 阈值:入场±{entry_dev * 100:.3f}% | 止盈{tp * 100:.3f}% | 止损{sl * 100:.3f}%\n" + f"🌊 波动率:ATR={atr_ratio * 100:.3f}% | 基准={base_ratio * 100:.3f}% | 缩放={vol_scale:.2f}\n" + 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: + if self.pos != 0: msg += ( - f"📈 当前盈亏:毛盈亏={current_gross_pnl * 100:.3f}% | " - f"净盈亏={current_net_pnl * 100:.3f}% | " + f"📈 当前盈亏:毛={current_gross_pnl * 100:.3f}% | 净={current_net_pnl * 100:.3f}% | " f"手续费={current_fee:.4f}u\n" - f"📍 入场价:{self.entry_price:.2f} | " - f"仓位价值:{self.entry_position_value:.2f}u\n" + f"⚠️ 连续亏损:{self.consecutive_losses}次\n" ) - msg += f"⏱️ 持仓限制:{self.cfg.max_hold_sec}s | 冷却:{self.cfg.cooldown_sec_after_exit}s" - self.ding(msg) - def openBrowser(self): - """打开 TGE 对应浏览器实例""" - try: - bit_port = openBrowser(id=self.bit_id) - co = ChromiumOptions() - co.set_local_port(port=bit_port) - - self.page = ChromiumPage(addr_or_opts=co) - return True - except: - return False - - def take_over_browser(self): - """接管浏览器""" - try: - co = ChromiumOptions() - co.set_local_port(self.tge_port) - self.page = ChromiumPage(addr_or_opts=co) - self.page.set.window.max() - return True - except: - return False - - def close_extra_tabs(self): - """关闭多余 tab""" - try: - for idx, tab in enumerate(self.page.get_tabs()): - if idx > 0: - tab.close() - return True - except: - return False - - def click_safe(self, xpath, sleep=0.5): - """安全点击""" - try: - ele = self.page.ele(xpath) - if not ele: - return False - ele.scroll.to_see(center=True) - time.sleep(sleep) - ele.click() - return True - except: - return False - - def 平仓(self): - self.click_safe('x://span[normalize-space(text()) ="市价"]') - - def 开单(self, marketPriceLongOrder=0, limitPriceShortOrder=0, size=None, price=None): - """ - marketPriceLongOrder 市价最多或者做空,1是做多,-1是做空 - limitPriceShortOrder 限价最多或者做空 - """ - - if marketPriceLongOrder == -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 marketPriceLongOrder == 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()) ="买入/做多"]') - - if limitPriceShortOrder == -1: - self.click_safe('x://button[normalize-space(text()) ="限价"]') - self.page.ele('x://*[@id="price_0"]').input(vals=price, clear=True) - time.sleep(1) - self.page.ele('x://*[@id="size_0"]').input(1) - self.click_safe('x://span[normalize-space(text()) ="卖出/做空"]') - elif limitPriceShortOrder == 1: - self.click_safe('x://button[normalize-space(text()) ="限价"]') - self.page.ele('x://*[@id="price_0"]').input(vals=price, clear=True) - time.sleep(1) - self.page.ele('x://*[@id="size_0"]').input(1) - self.click_safe('x://span[normalize-space(text()) ="买入/做多"]') - + # ----------------- 主循环 ----------------- def action(self): """主循环""" if not self.set_leverage(): self.ding("杠杆设置失败,停止运行", error=True) return - # 1. 打开浏览器 + # 打开浏览器 if not self.openBrowser(): self.ding("打开 TGE 失败!", error=True) return logger.info("TGE 端口获取成功") - logger.info("浏览器接管成功") self.page.get("https://derivatives.bitmart.com/zh-CN/futures/ETHUSDT") + time.sleep(3) # 等待页面加载 + + logger.info("策略启动 - 90%返佣优化版") + logger.info("策略特点:低阈值、高频次、严格方向过滤") while True: now_dt = datetime.datetime.now() @@ -1178,14 +1059,12 @@ class BitmartFuturesMeanReversionBot: last_k = klines[-1] closes = [k["close"] for k in klines[-(self.cfg.ema_len + 1):]] ema_value = self.ema(closes, self.cfg.ema_len) - price = self.get_last_price(fallback_close=float(last_k["close"])) dev = (price - ema_value) / ema_value if ema_value else 0.0 - # 3. 计算波动率相关指标 + # 3. 计算波动率指标 a = self.atr(klines, self.cfg.atr_len) atr_ratio = (a / price) if price > 0 else 0.0 - base_ratio = self.get_base_ratio_cached(klines) # 4. 计算动态阈值 @@ -1193,13 +1072,6 @@ class BitmartFuturesMeanReversionBot: atr_ratio, base_ratio ) - # 记录调试信息 - logger.debug( - f"循环数据: price={price:.2f}, ema={ema_value:.2f}, dev={dev * 100:.3f}%, " - f"atr_ratio={atr_ratio * 100:.3f}%, base_ratio={base_ratio * 100:.3f}%, " - f"entry_dev={entry_dev * 100:.3f}%" - ) - # 5. 风控检查 self.risk_kill_switch() @@ -1214,21 +1086,20 @@ class BitmartFuturesMeanReversionBot: if self.close_position_all(): logger.info("交易被禁用,已平仓") else: - logger.error("交易被禁用,但平仓失败,请手动检查!") - self.ding("⚠️ 交易被禁用但平仓失败,请手动检查持仓!", error=True) - logger.warning("交易被禁用(风控触发),等待...") + logger.error("交易被禁用,但平仓失败") + logger.warning("交易被禁用,等待...") time.sleep(5) continue # 8. 检查危险市场 if self.is_danger_market(klines, price): - logger.warning("危险模式:高波动/大实体K,暂停开仓") + logger.warning("危险模式,暂停开仓") self.maybe_exit(price, ema_value, tp, sl, vol_scale) time.sleep(self.cfg.tick_refresh_sec) continue # 9. 执行交易逻辑 - # 先检查平仓(包括正常平仓、止盈、止损) + # 先检查平仓 self.maybe_exit(price, ema_value, tp, sl, vol_scale) # 再检查开仓 self.maybe_enter(price, ema_value, entry_dev) @@ -1261,10 +1132,28 @@ if __name__ == "__main__": cfg = StrategyConfig() bot = BitmartFuturesMeanReversionBot(cfg, bit_id="f2320f57e24c45529a009e1541e25961") - # 设置日志级别为INFO以便查看详细计算过程 + # 设置日志级别 logger.remove() logger.add(lambda msg: tqdm.write(msg, end=""), level="INFO") + logger.info(""" + ==================================================== + 90%返佣优化策略说明: + + 策略特点: + 1. 针对90%返佣优化,阈值降低30-50% + 2. 增加方向过滤,避免平仓后立即反向开仓 + 3. 冷却时间从20秒减至5秒,提高交易频率 + 4. 添加连续亏损控制和趋势过滤 + 5. 日内交易次数限制:50次 + + 核心修复: + 1. 避免平仓后立即反向开仓(原策略最大问题) + 2. 修复平仓后可能立即开反向仓位的问题 + 3. 增加对同方向冷却的控制 + ==================================================== + """) + try: bot.action() except KeyboardInterrupt: diff --git a/test1.py b/test1.py index b5fce02..66700f4 100644 --- a/test1.py +++ b/test1.py @@ -1,15 +1,17 @@ -import os import time import uuid import datetime -from dataclasses import dataclass +import requests +from typing import Tuple from tqdm import tqdm from loguru import logger - +from dataclasses import dataclass from bitmart.api_contract import APIContract +from DrissionPage import ChromiumPage, ChromiumOptions from bitmart.lib.cloud_exceptions import APIException +from bit_tools import openBrowser from 交易.tools import send_dingtalk_message @@ -22,40 +24,68 @@ class StrategyConfig: # ===== 合约 ===== contract_symbol: str = "ETHUSDT" open_type: str = "cross" - leverage: str = "30" # 50 -> 30:显著降低1m噪声导致的连环止损与回撤波动 + leverage: str = "30" # ===== K线与指标 ===== step_min: int = 1 lookback_min: int = 240 - ema_len: int = 36 # 30 -> 36:均值更稳,信号更挑剔 + ema_len: int = 36 atr_len: int = 14 - # ===== 动态阈值基础(自适应行情)===== - entry_dev_floor: float = 0.0012 # 0.10% -> 0.12%:过滤小噪声进场 - tp_floor: float = 0.0006 # 0.05% -> 0.06%:更接近“净盈利” - sl_floor: float = 0.0018 # 0.15% -> 0.18%:ETH 1m插针多,底线略放宽 + # ========================================================= + # ✅ 自动阈值:ATR/Price 分位数基准(更稳,不被短时噪声带跑) + # ========================================================= + vol_baseline_window: int = 60 + vol_baseline_quantile: float = 0.65 + vol_scale_min: float = 0.80 + vol_scale_max: float = 1.60 - # 更挑剔、更少止损(进场更苛刻;止损不过度随波动放大) - entry_k: float = 1.45 # 1.20 -> 1.45:减少进场频率 - tp_k: float = 0.65 # 0.60 -> 0.65:略抬止盈 - sl_k: float = 1.05 # 1.20 -> 1.05:配合sl_floor,避免高波动下止损无限变大 + # ✅ baseline 每 60 秒刷新一次(体感更明显、也省CPU) + base_ratio_refresh_sec: int = 60 + + # ========================================================= + # ✅ 动态 floor(方案一) + # floor = clamp(min, base_k * base_ratio, max) + # 目的:跟着典型波动变,过滤小噪声;tp/sl 也随环境自适应 + # ========================================================= + # entry_dev_floor 动态 + entry_dev_floor_min: float = 0.0012 # 0.12% + entry_dev_floor_max: float = 0.0030 # 0.30%(可按你偏好调) + entry_dev_floor_base_k: float = 1.10 # entry_floor = 1.10 * base_ratio + + # tp_floor 动态 + tp_floor_min: float = 0.0006 # 0.06% + tp_floor_max: float = 0.0020 # 0.20% + tp_floor_base_k: float = 0.55 # tp_floor = 0.55 * base_ratio(止盈别太大,1m回归更实际) + + # sl_floor 动态 + sl_floor_min: float = 0.0018 # 0.18% + sl_floor_max: float = 0.0060 # 0.60% + sl_floor_base_k: float = 1.35 # sl_floor = 1.35 * base_ratio(ETH 1m 插针多,止损下限可更稳) + + # ========================================================= + # ✅ 动态阈值倍率(仍然保留你原来思路) + # ========================================================= + entry_k: float = 1.45 + tp_k: float = 0.65 + sl_k: float = 1.05 # ===== 时间/冷却 ===== - max_hold_sec: int = 75 # 90/120 -> 75:1m回归不恋战 - cooldown_sec_after_exit: int = 20 # 10 -> 20:减少“刚出又进”连环单 + max_hold_sec: int = 75 + cooldown_sec_after_exit: int = 20 # ===== 下单/仓位 ===== - risk_percent: float = 0.004 # 0.005 -> 0.004:再压一点波动,更贴合止损≤5/日 + risk_percent: float = 0.004 min_size: int = 1 max_size: int = 5000 # ===== 日内风控 ===== - daily_loss_limit: float = 0.02 # -2% 停机 - daily_profit_cap: float = 0.01 # +1% 封顶停机 + daily_loss_limit: float = 0.02 + daily_profit_cap: float = 0.01 - # ===== 危险模式过滤(1m ETH 更敏感)===== - atr_ratio_kill: float = 0.0038 # 0.0045 -> 0.0038:更早暂停开仓 - big_body_kill: float = 0.010 # 0.012 -> 0.010:更敏感 + # ===== 危险模式过滤 ===== + atr_ratio_kill: float = 0.0038 + big_body_kill: float = 0.010 # ===== 轮询节奏 ===== klines_refresh_sec: int = 10 @@ -65,36 +95,45 @@ class StrategyConfig: # ========================================================= # ✅ 止损后同向入场加门槛(但不禁止同向重入) # ========================================================= - reentry_penalty_mult: float = 1.55 # 同向入场门槛×1.55:大幅降低连环止损概率 - reentry_penalty_max_sec: int = 180 # 罚时最长持续 - reset_band_k: float = 0.45 # dev回到更靠近均值才解除罚则 - reset_band_floor: float = 0.0006 # 最小复位带宽(0.06%) + reentry_penalty_mult: float = 1.55 + reentry_penalty_max_sec: int = 180 + reset_band_k: float = 0.45 + reset_band_floor: float = 0.0006 # ========================================================= - # ✅ 自动阈值:ATR/Price 分位数基准(更稳,不被短时噪声带跑) + # ✅ 止损后同方向 SL 放宽幅度与"止损时 vol_scale"联动 # ========================================================= - vol_baseline_window: int = 120 - vol_baseline_quantile: float = 0.65 - vol_scale_min: float = 0.80 - vol_scale_max: float = 1.60 - - # ========================================================= - # ✅ 升级:止损后同方向 SL 放宽幅度与“止损时 vol_scale”联动 - # ========================================================= - post_sl_sl_max_sec: int = 90 # 只照顾“扫损后很快反弹”的窗口 + post_sl_sl_max_sec: int = 90 post_sl_mult_min: float = 1.02 post_sl_mult_max: float = 1.16 - post_sl_vol_alpha: float = 0.20 # mult = 1 + alpha*(vol_scale_at_sl - 1) + 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): + def __init__(self, cfg: StrategyConfig, bit_id=None): + + self.bit_id = bit_id + + self.page: ChromiumPage | None = None + self.cfg = cfg - # ✅ 只从环境变量读(请务必更换曾经硬编码泄露过的 key) - self.api_key = os.getenv("BITMART_API_KEY", "").strip() - self.secret_key = os.getenv("BITMART_SECRET_KEY", "").strip() - self.memo = os.getenv("BITMART_MEMO", "合约交易").strip() + self.api_key = "a0fb7b98464fd9bcce67e7c519d58ec10d0c38a8" + self.secret_key = "4eaeba78e77aeaab1c2027f846a276d164f264a44c2c1bb1c5f3be50c8de1ca5" + self.memo = "合约交易" if not self.api_key or not self.secret_key: raise RuntimeError("请先设置环境变量 BITMART_API_KEY / BITMART_SECRET_KEY / BITMART_MEMO(可选)") @@ -107,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 @@ -117,22 +160,28 @@ class BitmartFuturesMeanReversionBot: self._klines_cache_ts = 0 self._last_status_notify_ts = 0 - # ✅ 止损后“同向入场加门槛”状态 + # ✅ base_ratio 缓存 + self._base_ratio_cached = 0.0015 # 初始化默认值 0.15% + self._base_ratio_ts = 0.0 + + # ✅ 止损后"同向入场加门槛"状态 self.last_sl_dir = 0 # 1=多止损,-1=空止损,0=无 self.last_sl_ts = 0.0 - # ✅ 止损后“同方向 SL 联动放宽”状态 + # ✅ 止损后"同方向 SL 联动放宽"状态 self.post_sl_dir = 0 self.post_sl_ts = 0.0 self.post_sl_vol_scale = 1.0 # 记录止损时的 vol_scale self.pbar = tqdm(total=60, desc="运行中(秒)", ncols=90) + logger.info(f"初始化完成,基准波动率默认值: {self._base_ratio_cached * 100:.4f}%") + # ----------------- 通用工具 ----------------- def ding(self, msg, error=False): prefix = "❌bitmart:" if error else "🔔bitmart:" if error: - for _ in range(3): + for _ in range(1): send_dingtalk_message(f"{prefix}{msg}") else: send_dingtalk_message(f"{prefix}{msg}") @@ -201,9 +250,6 @@ class BitmartFuturesMeanReversionBot: return None def get_last_price(self, fallback_close: float) -> float: - """ - 优先取更实时的最新价;若SDK不支持/字段不同,回退到K线close。 - """ try: if hasattr(self.contractAPI, "get_contract_details"): r = self.contractAPI.get_contract_details(contract_symbol=self.cfg.contract_symbol)[0] @@ -263,43 +309,176 @@ class BitmartFuturesMeanReversionBot: return False def atr_ratio_baseline(self, klines) -> float: - """ - 自动阈值基准:最近 window 根的 atr_ratio 分布的 quantile 作为“典型波动” - """ - window = self.cfg.vol_baseline_window - if len(klines) < (window + self.cfg.atr_len + 5): + """简化版ATR基准计算""" + window = min(self.cfg.vol_baseline_window, len(klines) - self.cfg.atr_len - 1) + if window <= 10: # 数据太少 + logger.warning(f"数据不足计算基准: {len(klines)}根K线") return 0.0 ratios = [] - for i in range(-window, 0): - sub = klines[:i] if i != 0 else klines - a = self.atr(sub, self.cfg.atr_len) - p = sub[-1]["close"] - if p > 0 and a > 0: - ratios.append(a / p) - if not ratios: - return 0.0 + # 简化计算:每隔3根K线计算一个ATR比率(减少计算量) + step = 3 + for i in range(-window, 0, step): + if len(klines) + i < self.cfg.atr_len + 1: + continue + # 计算当前位置的ATR + start_idx = len(klines) + i - self.cfg.atr_len + end_idx = len(klines) + i + + if start_idx < 0 or end_idx <= start_idx: + continue + + sub_klines = klines[start_idx:end_idx] + + # 确保有足够数据计算ATR + if len(sub_klines) >= self.cfg.atr_len + 1: + a = self.atr(sub_klines, self.cfg.atr_len) + price = klines[end_idx - 1]["close"] + + if a > 0 and price > 0: + ratio = a / price + if 0.0001 < ratio < 0.01: # 过滤异常值 + ratios.append(ratio) + + if len(ratios) < 5: # 样本太少 + # 尝试直接使用整个数据计算一个ATR比率 + a = self.atr(klines[-60:], self.cfg.atr_len) # 使用最近60根K线 + price = klines[-1]["close"] + if a > 0 and price > 0: + baseline = a / price + logger.debug(f"使用全量数据计算基准: {baseline * 100:.4f}%") + return baseline + else: + return 0.0 + + # 计算分位数 ratios.sort() - q = max(0.0, min(1.0, self.cfg.vol_baseline_quantile)) - idx = int(q * (len(ratios) - 1)) - return ratios[idx] + idx = min(len(ratios) - 1, + max(0, int(self.cfg.vol_baseline_quantile * (len(ratios) - 1)))) + baseline = ratios[idx] + + logger.debug(f"基准计算: 样本数={len(ratios)}, 基准={baseline * 100:.4f}%, " + f"范围=[{ratios[0] * 100:.4f}%, {ratios[-1] * 100:.4f}%]") + + return baseline + + def get_base_ratio_cached(self, klines) -> float: + """获取缓存的基准波动率,定期刷新""" + now = time.time() + refresh_sec = self.cfg.base_ratio_refresh_sec + + if (self._base_ratio_cached is None or + (now - self._base_ratio_ts) >= refresh_sec): + + # 使用简单版本的基准计算 + baseline = self.atr_ratio_baseline(klines) + + if baseline > 0.0001: # 大于0.01%才认为是有效值 + self._base_ratio_cached = baseline + self._base_ratio_ts = now + logger.info(f"基准波动率更新: {baseline * 100:.4f}%") + else: + # 使用基于价格的动态默认值 + current_price = klines[-1]["close"] if klines else 3000 + # ETH价格越高,基准波动率越小(百分比) + if current_price > 4000: + default_baseline = 0.0010 # 0.10% + elif current_price > 3500: + default_baseline = 0.0012 # 0.12% + elif current_price > 3000: + default_baseline = 0.0015 # 0.15% + elif current_price > 2500: + default_baseline = 0.0018 # 0.18% + else: + default_baseline = 0.0020 # 0.20% + + self._base_ratio_cached = default_baseline + self._base_ratio_ts = now + logger.warning(f"使用价格动态默认基准: {default_baseline * 100:.4f}% " + f"(价格=${current_price:.0f})") + + return self._base_ratio_cached + + @staticmethod + def _clamp(x: float, lo: float, hi: float) -> float: + """限制数值在指定范围内""" + return max(lo, min(hi, x)) def dynamic_thresholds(self, atr_ratio: float, base_ratio: float): """ - 动态阈值:atr_ratio * vol_scale,并带 floor + ✅ entry/tp/sl 全部动态(修复版): + - vol_scale:atr_ratio/base_ratio 限幅 + - floor:方案一 (floor = clamp(min, k*base_ratio, max)) + - 最终阈值:max(floor, k * vol_scale * atr_ratio) """ - if base_ratio <= 0: - vol_scale = 1.0 - else: - raw = atr_ratio / base_ratio - vol_scale = max(self.cfg.vol_scale_min, min(self.cfg.vol_scale_max, raw)) + # 1) 检查输入有效性 + if atr_ratio <= 0: + logger.warning(f"ATR比率异常: {atr_ratio}") + atr_ratio = 0.001 # 默认值 0.1% - entry_dev = max(self.cfg.entry_dev_floor, self.cfg.entry_k * vol_scale * atr_ratio) - tp = max(self.cfg.tp_floor, self.cfg.tp_k * vol_scale * atr_ratio) - sl = max(self.cfg.sl_floor, self.cfg.sl_k * vol_scale * atr_ratio) - return entry_dev, tp, sl, vol_scale + # 2) 如果base_ratio太小或无效,使用调整后的atr_ratio + if base_ratio < 0.0005: # 小于0.05%视为无效 + base_ratio = max(0.001, atr_ratio * 1.2) # 比当前ATR比率稍大 + logger.debug(f"基准太小,使用调整后的atr_ratio: {base_ratio * 100:.4f}%") + + # 3) vol_scale计算 + if base_ratio > 0: + raw_scale = atr_ratio / base_ratio + vol_scale = self._clamp(raw_scale, self.cfg.vol_scale_min, self.cfg.vol_scale_max) + logger.debug( + f"vol_scale: {raw_scale:.2f} → {vol_scale:.2f} (atr={atr_ratio * 100:.3f}%, base={base_ratio * 100:.3f}%)") + else: + vol_scale = 1.0 + logger.warning(f"基准无效,使用默认vol_scale=1.0") + + # 4) 动态floor计算 + # Entry floor + entry_floor_raw = self.cfg.entry_dev_floor_base_k * base_ratio + entry_floor = self._clamp( + entry_floor_raw, + self.cfg.entry_dev_floor_min, + self.cfg.entry_dev_floor_max, + ) + + # TP floor + tp_floor_raw = self.cfg.tp_floor_base_k * base_ratio + tp_floor = self._clamp( + tp_floor_raw, + self.cfg.tp_floor_min, + self.cfg.tp_floor_max, + ) + + # SL floor + sl_floor_raw = self.cfg.sl_floor_base_k * base_ratio + sl_floor = self._clamp( + sl_floor_raw, + self.cfg.sl_floor_min, + self.cfg.sl_floor_max, + ) + + # 5) 最终阈值计算 + entry_dev_atr_part = self.cfg.entry_k * vol_scale * atr_ratio + entry_dev = max(entry_floor, entry_dev_atr_part) + + tp_atr_part = self.cfg.tp_k * vol_scale * atr_ratio + tp = max(tp_floor, tp_atr_part) + + sl_atr_part = self.cfg.sl_k * vol_scale * atr_ratio + sl = max(sl_floor, sl_atr_part) + + # 6) 确保entry_dev不会太小 + entry_dev = max(entry_dev, self.cfg.entry_dev_floor_min) + + # 7) 输出详细信息 + logger.info( + f"动态阈值: atr={atr_ratio * 100:.4f}%, base={base_ratio * 100:.4f}%, " + f"vol_scale={vol_scale:.2f}, floor={entry_floor * 100:.4f}%, " + f"atr_part={entry_dev_atr_part * 100:.4f}%, 最终entry_dev={entry_dev * 100:.4f}%" + ) + + return entry_dev, tp, sl, vol_scale, entry_floor, tp_floor, sl_floor # ----------------- 账户/仓位 ----------------- def get_assets_available(self) -> float: @@ -372,80 +551,256 @@ class BitmartFuturesMeanReversionBot: # ----------------- 下单 ----------------- def calculate_size(self, price: float) -> int: """ - 保守仓位估算:按 1张≈0.001ETH(沿用你原假设) + 计算仓位大小 + 固定每单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: """ - side: - 1 开多 - 2 平空 - 3 平多 - 4 开空 + 【下单函数】实际执行下单操作 + side: 1=开多, 4=开空, 2=平多, 3=平空 """ if size <= 0: return False - client_order_id = f"mr_{int(time.time())}_{uuid.uuid4().hex[:8]}" try: - resp = self.contractAPI.post_submit_order( - contract_symbol=self.cfg.contract_symbol, - client_order_id=client_order_id, - side=side, - mode=1, - type="market", - leverage=self.cfg.leverage, - open_type=self.cfg.open_type, - size=size - )[0] - - logger.info(f"order_resp: {resp}") - - if resp.get("code") == 1000: + # 开多单 + if side == 1: + 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 - 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): - 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: - self.pos = 0 + 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: + 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: - """ - 止损后同向入场加门槛: - - 只要 dev 还没有回到中性区,就对“上次止损方向”的同向入场门槛提高 - - dev 回到 abs(dev) <= reset_band 后自动解除 - - 超过 max_sec 自动解除(避免一直卡住) - """ + """检查是否需要应用重新入场惩罚""" if self.last_sl_dir == 0: return False @@ -461,11 +816,7 @@ class BitmartFuturesMeanReversionBot: return True def _post_sl_dynamic_mult(self) -> float: - """ - 止损后同方向 SL 放宽倍数与“止损时 vol_scale”联动: - mult = 1 + alpha*(vol_scale_at_sl - 1) - 并做上下限裁剪 + 有效期控制 - """ + """计算止损后SL放宽倍数""" if self.post_sl_dir == 0: return 1.0 @@ -480,9 +831,16 @@ class BitmartFuturesMeanReversionBot: # ----------------- 交易逻辑 ----------------- def in_cooldown(self) -> bool: + """检查是否在冷却期内""" return (time.time() - self.last_exit_ts) < self.cfg.cooldown_sec_after_exit def maybe_enter(self, price: float, ema_value: float, entry_dev: float): + """ + 检查并执行入场 + 【开单入口函数】此函数负责判断是否开单以及开单方向 + - 开多:价格低于EMA超过阈值时 + - 开空:价格高于EMA超过阈值时 + """ if self.pos != 0: return if self.in_cooldown(): @@ -495,92 +853,177 @@ class BitmartFuturesMeanReversionBot: penalty_active = self._reentry_penalty_active(dev, entry_dev) - # 基础阈值 long_th = -entry_dev short_th = entry_dev - # 若罚则生效:对“上次止损方向”的同向阈值提高 if penalty_active: if self.last_sl_dir == 1: long_th = -entry_dev * self.cfg.reentry_penalty_mult + logger.info( + f"多头止损后惩罚生效: 入场阈值从 {long_th * 100:.3f}% 调整为 {(-entry_dev * self.cfg.reentry_penalty_mult) * 100:.3f}%") elif self.last_sl_dir == -1: short_th = entry_dev * self.cfg.reentry_penalty_mult + logger.info( + f"空头止损后惩罚生效: 入场阈值从 {short_th * 100:.3f}% 调整为 {(entry_dev * self.cfg.reentry_penalty_mult) * 100:.3f}%") logger.info( - f"enter_check: price={price:.2f}, ema={ema_value:.2f}, dev={dev * 100:.3f}% " + f"入场检查: price={price:.2f}, ema={ema_value:.2f}, dev={dev * 100:.3f}% " f"(entry_dev={entry_dev * 100:.3f}%, long_th={long_th * 100:.3f}%, short_th={short_th * 100:.3f}%) " f"size={size}, penalty={penalty_active}, last_sl_dir={self.last_sl_dir}" ) + # ========== 【开单位置1】开多单 ========== + # 方向:多(做多,买入) + # 条件:价格偏离EMA向下超过阈值 (dev <= long_th) if dev <= long_th: - if self.place_market_order(1, size): # 开多 + if self.place_market_order(1, size): # side=1 表示开多 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】开空单 ========== + # 方向:空(做空,卖出) + # 条件:价格偏离EMA向上超过阈值 (dev >= short_th) elif dev >= short_th: - if self.place_market_order(4, size): # 开空 + if self.place_market_order(4, size): # side=4 表示开空 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) - # ✅ 同方向止损后:在有效期内放宽 SL(与止损时 vol_scale 联动) + # 计算价格偏离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: - # 记录止损方向 - sl_dir = self.pos # 1=多止损,-1=空止损 + # ========== 【平仓条件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 - 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 - ) + # ========== 【平仓条件3】止损 ========== + # 止损使用毛盈亏判断(因为止损是风险控制,即使扣除手续费后亏损也要止损) + if gross_pnl <= -effective_sl: + sl_dir = self.pos - # ✅ 开启:同向入场加门槛 - self.last_sl_dir = sl_dir - self.last_sl_ts = time.time() + 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 + ) - # ✅ 开启:同向 SL 联动放宽(记录止损时 vol_scale) - self.post_sl_dir = sl_dir - self.post_sl_ts = time.time() - self.post_sl_vol_scale = float(vol_scale) + self.last_sl_dir = sl_dir + self.last_sl_ts = time.time() - self.entry_price, self.entry_ts = None, None - self.last_exit_ts = time.time() + self.post_sl_dir = sl_dir + self.post_sl_ts = time.time() + self.post_sl_vol_scale = float(vol_scale) - 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() + 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 + + # ========== 【平仓条件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, - entry_dev: float, tp: float, sl: float): + entry_dev: float, tp: float, sl: float, + entry_floor: float, tp_floor: float, sl_floor: float): + """限频状态通知""" now = time.time() if (now - self._last_status_notify_ts) < self.cfg.status_notify_sec: return @@ -593,35 +1036,151 @@ class BitmartFuturesMeanReversionBot: if self.pos != 0 and self.post_sl_dir == self.pos: sl_mult = self._post_sl_dynamic_mult() + 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" - f"现价:{price:.2f}\n" - f"EMA{self.cfg.ema_len}:{ema_value:.2f}\n" - f"dev:{dev * 100:.3f}%(entry_dev={entry_dev * 100:.3f}%)\n" - f"ATR比:{atr_ratio * 100:.3f}% 基准:{base_ratio * 100:.3f}% vol_scale={vol_scale:.2f}\n" - f"tp/sl:{tp * 100:.3f}% / {sl * 100:.3f}%(postSL×{sl_mult:.2f}, sl@scale={self.post_sl_vol_scale:.2f})\n" - f"止损同向加门槛:{'ON' if penalty_active else 'OFF'}(last_sl_dir={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"【BitMart {self.cfg.contract_symbol}|1m均值回归(动态阈值)】\n" + f"📊 状态:{direction_str}\n" + f"💰 现价:{price:.2f} | EMA{self.cfg.ema_len}:{ema_value:.2f}\n" + f"📈 偏离:{dev * 100:.3f}% (入场阈值:±{entry_dev * 100:.3f}%)\n" + 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 | 固定保证金:{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): + """打开 TGE 对应浏览器实例""" + try: + bit_port = openBrowser(id=self.bit_id) + co = ChromiumOptions() + co.set_local_port(port=bit_port) + + self.page = ChromiumPage(addr_or_opts=co) + return True + except: + return False + + def take_over_browser(self): + """接管浏览器""" + try: + co = ChromiumOptions() + co.set_local_port(self.tge_port) + self.page = ChromiumPage(addr_or_opts=co) + self.page.set.window.max() + return True + except: + return False + + def close_extra_tabs(self): + """关闭多余 tab""" + try: + for idx, tab in enumerate(self.page.get_tabs()): + if idx > 0: + tab.close() + return True + except: + return False + + def click_safe(self, xpath, sleep=0.5): + """安全点击""" + try: + ele = self.page.ele(xpath) + if not ele: + return False + ele.scroll.to_see(center=True) + time.sleep(sleep) + ele.click() + return True + except: + return False + + def 平仓(self): + self.click_safe('x://span[normalize-space(text()) ="市价"]') + + def 开单(self, marketPriceLongOrder=0, limitPriceShortOrder=0, size=None, price=None): + """ + marketPriceLongOrder 市价最多或者做空,1是做多,-1是做空 + limitPriceShortOrder 限价最多或者做空 + """ + + if marketPriceLongOrder == -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 marketPriceLongOrder == 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()) ="买入/做多"]') + + if limitPriceShortOrder == -1: + self.click_safe('x://button[normalize-space(text()) ="限价"]') + self.page.ele('x://*[@id="price_0"]').input(vals=price, clear=True) + time.sleep(1) + self.page.ele('x://*[@id="size_0"]').input(1) + self.click_safe('x://span[normalize-space(text()) ="卖出/做空"]') + elif limitPriceShortOrder == 1: + self.click_safe('x://button[normalize-space(text()) ="限价"]') + self.page.ele('x://*[@id="price_0"]').input(vals=price, clear=True) + time.sleep(1) + self.page.ele('x://*[@id="size_0"]').input(1) + self.click_safe('x://span[normalize-space(text()) ="买入/做多"]') + def action(self): + """主循环""" if not self.set_leverage(): self.ding("杠杆设置失败,停止运行", error=True) return + # 1. 打开浏览器 + if not self.openBrowser(): + self.ding("打开 TGE 失败!", error=True) + return + logger.info("TGE 端口获取成功") + + # # 2. 接管浏览器 + # if not self.take_over_browser(): + # self.ding("接管浏览器失败!", error=True) + # return + logger.info("浏览器接管成功") + self.page.get("https://derivatives.bitmart.com/zh-CN/futures/ETHUSDT") + while True: now_dt = datetime.datetime.now() self.pbar.n = now_dt.second self.pbar.refresh() + # 1. 获取K线数据 klines = self.get_klines_cached() if not klines or len(klines) < (self.cfg.ema_len + 5): + logger.warning("K线数据不足,等待...") time.sleep(1) continue + # 2. 计算技术指标 last_k = klines[-1] closes = [k["close"] for k in klines[-(self.cfg.ema_len + 1):]] ema_value = self.ema(closes, self.cfg.ema_len) @@ -629,44 +1188,64 @@ class BitmartFuturesMeanReversionBot: price = self.get_last_price(fallback_close=float(last_k["close"])) dev = (price - ema_value) / ema_value if ema_value else 0.0 - # 自动阈值 + # 3. 计算波动率相关指标 a = self.atr(klines, self.cfg.atr_len) atr_ratio = (a / price) if price > 0 else 0.0 - base_ratio = self.atr_ratio_baseline(klines) - entry_dev, tp, sl, vol_scale = self.dynamic_thresholds(atr_ratio, base_ratio) - # 日内风控 + base_ratio = self.get_base_ratio_cached(klines) + + # 4. 计算动态阈值 + entry_dev, tp, sl, vol_scale, entry_floor, tp_floor, sl_floor = self.dynamic_thresholds( + atr_ratio, base_ratio + ) + + # 记录调试信息 + logger.debug( + f"循环数据: price={price:.2f}, ema={ema_value:.2f}, dev={dev * 100:.3f}%, " + f"atr_ratio={atr_ratio * 100:.3f}%, base_ratio={base_ratio * 100:.3f}%, " + f"entry_dev={entry_dev * 100:.3f}%" + ) + + # 5. 风控检查 self.risk_kill_switch() - # 刷新仓位 + # 6. 获取持仓状态 if not self.get_position_status(): time.sleep(1) continue - # 停机:平仓+不再开仓 + # 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 - # 危险市场:不新开仓(允许已有仓按 tp/sl/超时 退出) + # 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 - # 先出场再入场 - self.maybe_exit(price, tp, sl, vol_scale) + # 9. 执行交易逻辑 + # 先检查平仓(包括正常平仓、止盈、止损) + self.maybe_exit(price, ema_value, tp, sl, vol_scale) + # 再检查开仓 self.maybe_enter(price, ema_value, entry_dev) - # 状态通知(限频) + # 10. 状态通知 bal = self.get_assets_available() self.notify_status_throttled( price, ema_value, dev, bal, atr_ratio, base_ratio, vol_scale, - entry_dev, tp, sl + entry_dev, tp, sl, + entry_floor, tp_floor, sl_floor ) time.sleep(self.cfg.tick_refresh_sec) @@ -683,10 +1262,22 @@ if __name__ == "__main__": Linux/macOS: export BITMART_API_KEY="你的key" export BITMART_SECRET_KEY="你的secret" - export BITMART_MEMO="合约交易" + export BITMART_MEMO "合约交易" """ cfg = StrategyConfig() - bot = BitmartFuturesMeanReversionBot(cfg) - bot.action() + bot = BitmartFuturesMeanReversionBot(cfg, bit_id="f2320f57e24c45529a009e1541e25961") + + # 设置日志级别为INFO以便查看详细计算过程 + logger.remove() + logger.add(lambda msg: tqdm.write(msg, end=""), level="INFO") + + try: + bot.action() + except KeyboardInterrupt: + logger.info("程序被用户中断") + bot.ding("🤖 策略已手动停止") + except Exception as e: + logger.error(f"程序异常退出: {e}") + bot.ding(f"❌ 策略异常退出: {e}", error=True) + raise -# 9208.96 diff --git a/推特/main.py b/推特/main.py index ba8d97f..5a16998 100644 --- a/推特/main.py +++ b/推特/main.py @@ -571,12 +571,15 @@ Win–win benefits — don’t miss out! 🚀 # 点击关注 # tab = self.page.new_tab(url="https://x.com/CryptoStart_App") - tab = self.page.new_tab(url="https://x.com/webseabusiness") + tab = self.page.new_tab(url="https://x.com/Websea_MY") time.sleep(10) - ele = tab.ele('x://span[text()="Follow"]', timeout=0.5) - if ele: - tab.actions.click(on_ele=ele) - time.sleep(10) + # ele = tab.ele('x://span[text()="Follow"]', timeout=0.5) + # if ele: + # tab.actions.click(on_ele=ele) + # time.sleep(10) + + tab.actions.click( + on_ele='x://*[@id="react-root"]/div/div/div[2]/main/div/div/div/div[1]/div/div[3]/div/div/div[1]/div[1]/div[1]/div[2]/div/div[1]/button/div/span/span') for i in range(random.randint(1, 10)): tab.actions.scroll(delta_y=random.randint(400, 800)) @@ -585,16 +588,17 @@ Win–win benefits — don’t miss out! 🚀 def 回复(self): urls = [ - "https://x.com/Websea_MY/status/2003700808186278388", - "https://x.com/Websea_MY/status/2003669526492405904", - # "https://x.com/Websea_MY/status/2001119115575222779", - # "https://x.com/Websea_MY/status/2001180999443734677" + "https://x.com/Websea_MY/status/2004081342364123146", + # "https://x.com/Websea_MY/status/2003669526492405904", + # # "https://x.com/Websea_MY/status/2001119115575222779", + # # "https://x.com/Websea_MY/status/2001180999443734677" ] tests = [ - "Canal premium de Websea: Regístrese para disfrutar de descuentos del 85 % en las comisiones de los futuros de Websea. Para grandes volúmenes de registros, el porcentaje es negociable. Agentes bienvenidos a consultar. Contacto Telegram: @webseatom", - "O canal premium da Websea oferece 85% de desconto nas taxas de comissão de contratos após o cadastro, com condições negociáveis para grandes volumes. Corretores interessados podem entrar em contato pelo Telegram: @webseatom", - "Канал Websea Premium: зарегистрируйтесь, чтобы получить 85% комиссионных от контрактов Websea. По запросу предоставляются скидки за объем. Приглашаем агентов обращаться с запросами. Контактный Telegram: @webseatom", - "Websea - Kênh hàng đầu, đăng ký ngay để nhận 85% hoa hồng từ hợp đồng Websea. Số lượng lớn có thể thương lượng, chào đón các đại lý liên hệ. Liên hệ TG: @webseatom" + "O seguro de futuros da Websea ultrapassou oficialmente a marca de 400 rodadas de airdrops! 🚀 Contacte TG@webseatom 85% de reembolso", + "Websea #FuturesInsurance официально преодолела отметку в 400 раундов аирдропов! 🚀Свяжитесь с TG@webseatom 85% кэшбэк", + "¡Websea #FuturesInsurance ha superado oficialmente las 400 rondas de airdrops! 🚀Póngase en contacto con TG@webseatom 85 % de reembolso", + "Websea #FuturesInsurance has officially surpassed 400 rounds of airdrops! 🚀 Contact TG@webseatom 85% cashback", + "Websea #FuturesInsurance đã chính thức vượt qua 400 đợt airdrop! 🚀Liên hệ TG@webseatom 85% hoàn tiền", ] tab = self.page.new_tab(random.choice(urls)) @@ -739,9 +743,9 @@ def run_work(x_token_info, xstart_info): pass # hub.to_do_tui() # 发推 - hub.回复() # 发推 + # hub.回复() # 发推 - # hub.FollowTwitterAccount() # 关注 + hub.FollowTwitterAccount() # 关注 if __name__ == '__main__': @@ -763,8 +767,8 @@ if __name__ == '__main__': # time.sleep(random.randint(15, 60)) # 同时运行 - max_threads = 10 - delay_between_start = 15 # 每次启动线程之间的延迟时间(秒) + max_threads = 5 + delay_between_start = 30 # 每次启动线程之间的延迟时间(秒) with ThreadPoolExecutor(max_workers=max_threads) as executor: @@ -785,5 +789,5 @@ if __name__ == '__main__': # continue executor.submit(run_work, x_token_info, xstart_info) - # time.sleep(random.randint(150, 850)) + # time.sleep(random.randint(60, 150)) time.sleep(delay_between_start)