From c6d7e5b6205cf15dcc1315d9b238a59429fc06ce Mon Sep 17 00:00:00 2001 From: Administrator Date: Wed, 24 Dec 2025 18:34:19 +0800 Subject: [PATCH] =?UTF-8?q?bitmart=E4=BC=98=E5=8C=96=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...易.py => 交易,一直加仓,拉平开仓价位.py} | 4 +- bitmart/优化拿仓版本.py | 458 +++++++++ bitmart/均线回归.py | 866 ++++++++++++++++++ bitmart/均线自动化开单.py | 302 ++++++ bitmart/框架.py | 20 +- telegram/8619211027341.session | Bin 45056 -> 45056 bytes telegram/bot_session.session | Bin 45056 -> 49152 bytes telegram/sign.db | Bin 40960 -> 40960 bytes 8 files changed, 1640 insertions(+), 10 deletions(-) rename bitmart/{交易.py => 交易,一直加仓,拉平开仓价位.py} (99%) create mode 100644 bitmart/优化拿仓版本.py create mode 100644 bitmart/均线回归.py create mode 100644 bitmart/均线自动化开单.py diff --git a/bitmart/交易.py b/bitmart/交易,一直加仓,拉平开仓价位.py similarity index 99% rename from bitmart/交易.py rename to bitmart/交易,一直加仓,拉平开仓价位.py index 553adb9..aec9701 100644 --- a/bitmart/交易.py +++ b/bitmart/交易,一直加仓,拉平开仓价位.py @@ -204,6 +204,8 @@ class BrowserTradingExecutor: def 开单(self, marketPriceLongOrder: int = 0, limitPriceShortOrder: int = 0, size: Optional[float] = None, price: Optional[float] = None) -> bool: + + size = 0.1 """ 开单操作(通过浏览器自动化,获取高返佣) @@ -968,7 +970,7 @@ if __name__ == '__main__': tge_id=196495, # TGE浏览器ID trading_url="https://derivatives.bitmart.com/zh-CN/futures/ETHUSDT", spread_percent=0.01, # 0.01%价差 - order_size_usdt=1.0, # 每单10 USDT + order_size_usdt=0.1, # 每单10 USDT max_position_usdt=3.0, # 最大持仓100 USDT order_refresh_interval=2.0, # 2秒刷新一次 order_timeout=60.0, # 60秒超时 diff --git a/bitmart/优化拿仓版本.py b/bitmart/优化拿仓版本.py new file mode 100644 index 0000000..489fcbb --- /dev/null +++ b/bitmart/优化拿仓版本.py @@ -0,0 +1,458 @@ +""" +BitMart 被动做市/高频刷单策略 (修复版 V2) +修复内容: +1. 修正 get_order_book 中解析深度数据的方式,由字典键名访问改为列表索引访问 (['price'] -> [0]) +""" + +import time +import requests +from typing import Optional, Dict, List, Tuple +from dataclasses import dataclass +from loguru import logger +from threading import Lock + +from DrissionPage import ChromiumPage, ChromiumOptions +from bitmart.api_contract import APIContract + + +# ================================================================ +# 📊 配置类 +# ================================================================ + +@dataclass +class MarketMakingConfig: + """做市策略配置""" + # API配置(仅用于查询,不下单) + api_key: str = "a0fb7b98464fd9bcce67e7c519d58ec10d0c38a8" + secret_key: str = "4eaeba78e77aeaab1c2027f846a276d164f264a44c2c1bb1c5f3be50c8de1ca5" + memo: str = "合约交易" + contract_symbol: str = "ETHUSDT" + + # 浏览器配置 + tge_id: int = 196495 # TGE浏览器ID + tge_url: str = "http://127.0.0.1:50326" + tge_headers: Dict = None + trading_url: str = "https://derivatives.bitmart.com/zh-CN/futures/ETHUSDT" + + # 做市基础参数 + spread_percent: float = 0.04 # 基础价差 (0.04% 约为 $1左右 on ETH) + order_size_usdt: float = 10.0 # 每单金额(USDT) + max_position_usdt: float = 100.0 # 最大持仓金额(USDT) + + # 🚀 高级策略参数 + # 库存倾斜:每持有100U,价格偏移多少。正数表示持有多单时价格下调(利于卖出,不利于买入) + inventory_skew_factor: float = 0.0005 + # 价格容忍度:只有当(目标价 - 当前挂单价) / 目标价 > 0.05% 时才改单,避免频繁操作 + price_tolerance: float = 0.0005 + + # 风险控制 + max_daily_loss: float = 50.0 + leverage: str = "30" + open_type: str = "cross" + + def __post_init__(self): + """初始化TGE headers""" + if self.tge_headers is None: + self.tge_headers = { + "Authorization": "Bearer asp_174003986c9b0799677c5b2c1adb76e402735d753bc91a91", + "Content-Type": "application/json" + } + + +# ================================================================ +# 📊 订单簿数据结构 +# ================================================================ + +@dataclass +class OrderBook: + """订单簿数据""" + bids: List[Tuple[float, float]] # [(价格, 数量), ...] + asks: List[Tuple[float, float]] # [(价格, 数量), ...] + timestamp: float + + @property + def mid_price(self) -> Optional[float]: + """中间价""" + if self.bids and self.asks: + return (self.bids[0][0] + self.asks[0][0]) / 2 + return None + + +# ================================================================ +# 📊 浏览器管理器 +# ================================================================ + +class BrowserManager: + """浏览器管理器:负责浏览器的启动、接管和标签页管理""" + + def __init__(self, config: MarketMakingConfig): + self.config = config + self.tge_port: Optional[int] = None + self.page: Optional[ChromiumPage] = None + + 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 + ) + data = response.json() + if "data" in data and "port" in data["data"]: + self.tge_port = data["data"]["port"] + logger.success(f"成功打开浏览器,端口:{self.tge_port}") + return True + else: + logger.error(f"打开浏览器响应异常: {data}") + return False + except Exception as e: + logger.error(f"打开浏览器失败: {e}") + return False + + def take_over_browser(self) -> bool: + """接管浏览器""" + if not self.tge_port: + logger.error("浏览器端口未设置") + return False + + try: + co = ChromiumOptions() + co.set_local_port(self.tge_port) + self.page = ChromiumPage(addr_or_opts=co) + logger.success("成功接管浏览器") + return True + except Exception as e: + logger.error(f"接管浏览器失败: {e}") + return False + + def close_extra_tabs(self) -> bool: + """关闭多余的标签页,只保留第一个""" + if not self.page: + return False + try: + tabs = self.page.get_tabs() + for idx, tab in enumerate(tabs): + if idx == 0: continue + tab.close() + return True + except Exception as e: + logger.warning(f"关闭多余标签页失败: {e}") + return False + + +# ================================================================ +# 📊 浏览器交易执行器 +# ================================================================ + +class BrowserTradingExecutor: + """浏览器交易执行器:通过浏览器自动化下单(获取高返佣)""" + + def __init__(self, page: ChromiumPage): + self.page = page + + 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"点击失败 {xpath}: {e}") + return False + + def 开单(self, marketPriceLongOrder: int = 0, limitPriceShortOrder: int = 0, + size: Optional[float] = None, price: Optional[float] = None) -> bool: + """开单操作""" + size = 0.1 + + try: + # 市价单 (代码略) + if marketPriceLongOrder == -1: pass + elif marketPriceLongOrder == 1: pass + + # 限价单 + if limitPriceShortOrder == -1: + # 限价做空 + if not self.click_safe('x://button[normalize-space(text()) ="限价"]'): return False + self.page.ele('x://*[ @id="price_0"]').input(vals=price, clear=True) + time.sleep(0.2) + self.page.ele('x://*[ @id="size_0"]').input(vals=size, clear=True) + if not self.click_safe('x://span[normalize-space(text()) ="卖出/做空"]'): return False + logger.success(f"浏览器下单: 限价做空 {size} @ {price}") + return True + + elif limitPriceShortOrder == 1: + # 限价做多 + if not self.click_safe('x://button[normalize-space(text()) ="限价"]'): return False + self.page.ele('x://*[ @id="price_0"]').input(vals=price, clear=True) + time.sleep(0.2) + self.page.ele('x://*[ @id="size_0"]').input(vals=size, clear=True) + if not self.click_safe('x://span[normalize-space(text()) ="买入/做多"]'): return False + logger.success(f"浏览器下单: 限价做多 {size} @ {price}") + return True + + return False + except Exception as e: + logger.error(f"开单异常: {e}") + return False + + def place_limit_order(self, side: str, price: float, size: float) -> bool: + """统一接口""" + if side == "buy": + return self.开单(limitPriceShortOrder=1, size=size, price=price) + else: + return self.开单(limitPriceShortOrder=-1, size=size, price=price) + + +# ================================================================ +# 📊 BitMart API 封装 (修复 get_order_book) +# ================================================================ + +class BitMartMarketMakerAPI: + """BitMart做市API封装(仅用于查询,不下单)""" + + def __init__(self, config: MarketMakingConfig): + self.config = config + self.contractAPI = APIContract( + config.api_key, + config.secret_key, + config.memo, + timeout=(5, 15) + ) + + def get_order_book(self) -> Optional[OrderBook]: + try: + # 移除不支持的 limit 参数 + response = self.contractAPI.get_depth(contract_symbol=self.config.contract_symbol)[0] + + if response.get('code') == 1000: + data = response.get('data', {}) + bids = [] + asks = [] + # 解析数据 + if isinstance(data, dict): + bids_raw = data.get('bids', []) + asks_raw = data.get('asks', []) + + # 修复:b 是列表 [price, size],不是字典 + for b in bids_raw[:10]: + # b[0] 是价格, b[1] 是数量 + bids.append((float(b[0]), float(b[1]))) + + for a in asks_raw[:10]: + # a[0] 是价格, a[1] 是数量 + asks.append((float(a[0]), float(a[1]))) + + if bids and asks: + return OrderBook(bids=bids, asks=asks, timestamp=time.time()) + else: + logger.warning(f"获取深度失败: {response}") + return None + except Exception as e: + logger.error(f"获取订单簿异常: {e}") + return None + + def get_position_net(self) -> float: + """获取净持仓 (多为正,空为负)""" + try: + response = self.contractAPI.get_position(contract_symbol=self.config.contract_symbol)[0] + if response.get('code') == 1000: + data = response.get('data', []) + if data: + pos = data[0] + current_amount = float(pos.get('current_amount', 0)) + position_type = int(pos.get('position_type', 0)) # 1多 2空 + if position_type == 1: return current_amount + if position_type == 2: return -current_amount + return 0.0 + except Exception as e: + logger.error(f"持仓查询异常: {e}") + return 0.0 + + def get_open_orders(self) -> List[Dict]: + """获取当前挂单""" + try: + resp = self.contractAPI.get_open_order(contract_symbol=self.config.contract_symbol)[0] + if resp.get("code") == 1000: + return resp.get("data", []) + return [] + except Exception as e: + logger.error(f"查询挂单异常: {e}") + return [] + + def cancel_order(self, order_id: str) -> bool: + """API撤单""" + try: + resp = self.contractAPI.post_cancel_order(contract_symbol=self.config.contract_symbol, order_id=order_id)[0] + return resp.get("code") == 1000 + except Exception as e: + logger.error(f"API撤单异常: {e}") + return False + + def set_leverage(self): + try: + self.contractAPI.post_submit_leverage(contract_symbol=self.config.contract_symbol, leverage=self.config.leverage, open_type=self.config.open_type) + except: + pass + + +# ================================================================ +# 🧠 策略核心 +# ================================================================ + +class MarketMakingStrategy: + """优化版被动做市策略""" + + def __init__(self, config: MarketMakingConfig): + self.config = config + self.api = BitMartMarketMakerAPI(config) + self.browser_manager = BrowserManager(config) + self.trading_executor: Optional[BrowserTradingExecutor] = None + self.running = False + + # 初始化流程 + if not self._initialize_browser(): + raise Exception("浏览器初始化失败") + self.api.set_leverage() + + def _initialize_browser(self) -> bool: + try: + if not self.browser_manager.open_browser(): return False + if not self.browser_manager.take_over_browser(): return False + self.browser_manager.close_extra_tabs() + + # 访问交易页 + logger.info(f"正在访问交易页: {self.config.trading_url}") + self.browser_manager.page.get(self.config.trading_url) + time.sleep(3) + + self.trading_executor = BrowserTradingExecutor(self.browser_manager.page) + logger.success("浏览器环境就绪") + return True + except Exception as e: + logger.error(f"浏览器初始化异常: {e}") + return False + + def calculate_target_prices(self, mid_price: float, net_position: float) -> Tuple[float, float]: + """核心算法:计算考虑了库存倾斜的目标买卖价""" + # 1. 基础价差的一半 + half_spread = mid_price * (self.config.spread_percent / 100) / 2 + + # 2. 库存倾斜调整 + skew_adjust = net_position * self.config.inventory_skew_factor * mid_price + + quote_mid = mid_price - skew_adjust + + target_bid = quote_mid - half_spread + target_ask = quote_mid + half_spread + + # 3. 价格修正 (防止穿仓) + if target_ask <= target_bid: + target_ask = target_bid + mid_price * 0.0001 + + return round(target_bid, 2), round(target_ask, 2) + + def reconcile_orders(self, target_bid: float, target_ask: float): + """调节逻辑:对比API实际挂单 vs 目标价格""" + open_orders = self.api.get_open_orders() + + current_bids = [] + current_asks = [] + + for o in open_orders: + side = o.get('side') + # 兼容API返回 + side_str = str(side).lower() + if side_str == '1' or 'buy' in side_str: + current_bids.append(o) + elif side_str == '2' or 'sell' in side_str: + current_asks.append(o) + + # --- 调节买单 --- + valid_bid_exists = False + for order in current_bids: + price = float(order.get('price', 0)) + diff_pct = abs(price - target_bid) / target_bid + + if diff_pct < self.config.price_tolerance: + valid_bid_exists = True + else: + logger.info(f"♻️ 买单价格偏离 (现{price} vs 标{target_bid}),撤单") + self.api.cancel_order(order.get('order_id') or order.get('id')) + + if not valid_bid_exists: + # 计算张数 + size_contract = self.config.order_size_usdt / target_bid / 0.01 + size_contract = max(1, int(size_contract)) + logger.info(f"➕ 补买单: {target_bid} (数量:{size_contract})") + self.trading_executor.place_limit_order("buy", target_bid, size_contract) + + # --- 调节卖单 --- + valid_ask_exists = False + for order in current_asks: + price = float(order.get('price', 0)) + diff_pct = abs(price - target_ask) / target_ask + + if diff_pct < self.config.price_tolerance: + valid_ask_exists = True + else: + logger.info(f"♻️ 卖单价格偏离 (现{price} vs 标{target_ask}),撤单") + self.api.cancel_order(order.get('order_id') or order.get('id')) + + if not valid_ask_exists: + size_contract = self.config.order_size_usdt / target_ask / 0.01 + size_contract = max(1, int(size_contract)) + logger.info(f"➕ 补卖单: {target_ask} (数量:{size_contract})") + self.trading_executor.place_limit_order("sell", target_ask, size_contract) + + def run(self): + self.running = True + logger.info("🚀 策略已启动") + + while self.running: + try: + # 1. 获取市场数据 + ob = self.api.get_order_book() + if not ob: + time.sleep(1) + continue + mid_price = ob.mid_price + + # 2. 获取持仓 + net_position = self.api.get_position_net() + + # 3. 计算目标价 + t_bid, t_ask = self.calculate_target_prices(mid_price, net_position) + logger.info(f"Mid:{mid_price:.2f} | Pos:{net_position} | Target Bid:{t_bid} Ask:{t_ask}") + + # 4. 调节挂单 + self.reconcile_orders(t_bid, t_ask) + + # 5. 循环间隔 + time.sleep(3) + + except KeyboardInterrupt: + logger.warning("停止策略") + break + except Exception as e: + logger.error(f"Loop Exception: {e}") + time.sleep(2) + +if __name__ == '__main__': + config = MarketMakingConfig( + contract_symbol="ETHUSDT", + spread_percent=0.04, + order_size_usdt=0.1, + max_position_usdt=50.0, + inventory_skew_factor=0.0005, + price_tolerance=0.0005 + ) + + strategy = MarketMakingStrategy(config) + strategy.run() diff --git a/bitmart/均线回归.py b/bitmart/均线回归.py new file mode 100644 index 0000000..9616cce --- /dev/null +++ b/bitmart/均线回归.py @@ -0,0 +1,866 @@ +import os +import time +import uuid +import datetime +from dataclasses import dataclass + +from tqdm import tqdm +from loguru import logger + +from bitmart.api_contract import APIContract +from bitmart.lib.cloud_exceptions import APIException + +from 交易.tools import send_dingtalk_message + + +@dataclass +class StrategyConfig: + # ============================= + # 1m | ETH 永续 | 控止损≤5/日 + # ============================= + + # ===== 合约 ===== + contract_symbol: str = "ETHUSDT" + open_type: str = "cross" + leverage: str = "30" + + # ===== K线与指标 ===== + step_min: int = 1 + lookback_min: int = 240 + 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 + + # 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 + cooldown_sec_after_exit: int = 20 + + # ===== 下单/仓位 ===== + risk_percent: float = 0.004 + min_size: int = 1 + max_size: int = 5000 + + # ===== 日内风控 ===== + daily_loss_limit: float = 0.02 + daily_profit_cap: float = 0.01 + + # ===== 危险模式过滤 ===== + atr_ratio_kill: float = 0.0038 + big_body_kill: float = 0.010 + + # ===== 轮询节奏 ===== + klines_refresh_sec: int = 10 + 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 + + +class BitmartFuturesMeanReversionBot: + def __init__(self, cfg: StrategyConfig): + self.cfg = cfg + + 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(可选)") + + 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.day_start_equity = None + self.trading_enabled = True + self.day_tag = datetime.date.today() + + # 缓存 + self._klines_cache = None + 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 联动放宽"状态 + 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): + send_dingtalk_message(f"{prefix}{msg}") + else: + send_dingtalk_message(f"{prefix}{msg}") + + def set_leverage(self) -> bool: + try: + resp = self.contractAPI.post_submit_leverage( + contract_symbol=self.cfg.contract_symbol, + leverage=self.cfg.leverage, + open_type=self.cfg.open_type + )[0] + if resp.get("code") == 1000: + 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): + 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 + + kl = self.get_klines() + if kl: + self._klines_cache = kl + self._klines_cache_ts = now + return self._klines_cache + + def get_klines(self): + try: + end_time = int(time.time()) + start_time = end_time - 60 * self.cfg.lookback_min + + resp = self.contractAPI.get_kline( + contract_symbol=self.cfg.contract_symbol, + step=self.cfg.step_min, + start_time=start_time, + end_time=end_time + )[0] + + if resp.get("code") != 1000: + logger.error(f"获取K线失败: {resp}") + return None + + data = resp.get("data", []) + formatted = [] + for k in data: + formatted.append({ + "id": int(k["timestamp"]), + "open": float(k["open_price"]), + "high": float(k["high_price"]), + "low": float(k["low_price"]), + "close": float(k["close_price"]), + }) + formatted.sort(key=lambda x: x["id"]) + 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] + d = r.get("data") if isinstance(r, dict) else None + if isinstance(d, dict): + for key in ("last_price", "mark_price", "index_price"): + if key in d and d[key] is not None: + return float(d[key]) + + if hasattr(self.contractAPI, "get_ticker"): + r = self.contractAPI.get_ticker(contract_symbol=self.cfg.contract_symbol)[0] + d = r.get("data") if isinstance(r, dict) else None + if isinstance(d, dict): + for key in ("last_price", "price", "last", "close"): + if key in d and d[key] is not None: + return float(d[key]) + except Exception: + pass + + return float(fallback_close) + + @staticmethod + def ema(values, n: int) -> float: + k = 2 / (n + 1) + e = values[0] + for v in values[1:]: + e = v * k + e * (1 - k) + return e + + @staticmethod + def atr(klines, n: int) -> float: + if len(klines) < n + 1: + return 0.0 + trs = [] + for i in range(-n, 0): + cur = klines[i] + prev = klines[i - 1] + tr = max( + cur["high"] - cur["low"], + abs(cur["high"] - prev["close"]), + abs(cur["low"] - prev["close"]), + ) + trs.append(tr) + return sum(trs) / len(trs) + + def is_danger_market(self, klines, 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: + 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: + return True + + return False + + def atr_ratio_baseline(self, klines) -> float: + """简化版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 = [] + + # 简化计算:每隔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() + 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): + """ + ✅ 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) 检查输入有效性 + if atr_ratio <= 0: + logger.warning(f"ATR比率异常: {atr_ratio}") + atr_ratio = 0.001 # 默认值 0.1% + + # 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: + try: + resp = self.contractAPI.get_assets_detail()[0] + if resp.get("code") != 1000: + return 0.0 + data = resp.get("data") + if isinstance(data, dict): + return float(data.get("available_balance", 0)) + if isinstance(data, list): + for asset in data: + if asset.get("currency") == "USDT": + return float(asset.get("available_balance", 0)) + return 0.0 + except Exception as e: + logger.error(f"余额查询异常: {e}") + 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: + return False + + positions = resp.get("data", []) + if not positions: + self.pos = 0 + return True + + p = positions[0] + self.pos = 1 if p["position_type"] == 1 else -1 + 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}):重置日内风控基准") + + def risk_kill_switch(self): + self.refresh_daily_baseline() + equity = self.get_equity_proxy() + if equity <= 0: + return + + if self.day_start_equity is None: + self.day_start_equity = equity + logger.info(f"日内权益基准设定:{equity:.2f} USDT") + return + + pnl = (equity - self.day_start_equity) / self.day_start_equity + if pnl <= -self.cfg.daily_loss_limit: + self.trading_enabled = False + self.ding(f"触发日止损:{pnl * 100:.2f}% -> 停机", error=True) + + if pnl >= self.cfg.daily_profit_cap: + self.trading_enabled = False + self.ding(f"达到日盈利封顶:{pnl * 100:.2f}% -> 停机") + + # ----------------- 下单 ----------------- + def calculate_size(self, price: float) -> int: + bal = self.get_assets_available() + if bal < 10: + return 0 + + margin = bal * self.cfg.risk_percent + lev = int(self.cfg.leverage) + + # ⚠️ 沿用你的原假设:1张≈0.001ETH + size = int((margin * lev) / (price * 0.001)) + size = max(self.cfg.min_size, size) + size = min(self.cfg.max_size, size) + return size + + def place_market_order(self, side: int, size: int) -> bool: + 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: + return True + + self.ding(f"下单失败: {resp}", error=True) + return False + + except APIException as e: + logger.error(f"API下单异常: {e}") + self.ding(f"API下单异常: {e}", error=True) + return False + + except Exception as e: + 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 _reentry_penalty_active(self, dev: float, entry_dev: float) -> bool: + """检查是否需要应用重新入场惩罚""" + if self.last_sl_dir == 0: + return False + + if (time.time() - self.last_sl_ts) > self.cfg.reentry_penalty_max_sec: + self.last_sl_dir = 0 + return False + + reset_band = max(self.cfg.reset_band_floor, self.cfg.reset_band_k * entry_dev) + if abs(dev) <= reset_band: + self.last_sl_dir = 0 + return False + + return True + + def _post_sl_dynamic_mult(self) -> float: + """计算止损后SL放宽倍数""" + if self.post_sl_dir == 0: + return 1.0 + + if (time.time() - self.post_sl_ts) > self.cfg.post_sl_sl_max_sec: + self.post_sl_dir = 0 + self.post_sl_vol_scale = 1.0 + return 1.0 + + raw = 1.0 + self.cfg.post_sl_vol_alpha * (self.post_sl_vol_scale - 1.0) + 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): + """检查并执行入场""" + if self.pos != 0: + return + if self.in_cooldown(): + 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) + + 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}" + ) + + if dev <= long_th: + if self.place_market_order(1, size): + self.pos = 1 + self.entry_price = price + self.entry_ts = time.time() + self.ding(f"✅开多:dev={dev * 100:.3f}% size={size} entry={price:.2f}") + + elif dev >= short_th: + if self.place_market_order(4, size): + self.pos = -1 + self.entry_price = price + self.entry_ts = time.time() + self.ding(f"✅开空:dev={dev * 100:.3f}% size={size} entry={price:.2f}") + + def maybe_exit(self, price: float, tp: float, sl: float, vol_scale: float): + """检查并执行出场""" + 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 + + 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() + + elif 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 + ) + + 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.last_exit_ts = time.time() + + 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() + + 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_floor: float, tp_floor: float, sl_floor: float): + """限频状态通知""" + now = time.time() + if (now - self._last_status_notify_ts) < self.cfg.status_notify_sec: + return + 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 + + 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.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" + ) + self.ding(msg) + + def action(self): + """主循环""" + if not self.set_leverage(): + self.ding("杠杆设置失败,停止运行", error=True) + return + + 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) + + 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.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() + logger.warning("交易被禁用(风控触发),等待...") + time.sleep(5) + continue + + # 8. 检查危险市场 + if self.is_danger_market(klines, price): + logger.warning("危险模式:高波动/大实体K,暂停开仓") + self.maybe_exit(price, tp, sl, vol_scale) + time.sleep(self.cfg.tick_refresh_sec) + continue + + # 9. 执行交易逻辑 + self.maybe_exit(price, 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_floor, tp_floor, sl_floor + ) + + time.sleep(self.cfg.tick_refresh_sec) + + +if __name__ == "__main__": + """ + Windows PowerShell: + setx BITMART_API_KEY "你的key" + setx BITMART_SECRET_KEY "你的secret" + setx BITMART_MEMO "合约交易" + 重新打开终端再运行。 + + Linux/macOS: + export BITMART_API_KEY="你的key" + export BITMART_SECRET_KEY="你的secret" + export BITMART_MEMO "合约交易" + """ + cfg = StrategyConfig() + bot = BitmartFuturesMeanReversionBot(cfg) + + # 设置日志级别为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 + +# 目前动态计算阀值的速度是多少 \ No newline at end of file diff --git a/bitmart/均线自动化开单.py b/bitmart/均线自动化开单.py new file mode 100644 index 0000000..0dfdcef --- /dev/null +++ b/bitmart/均线自动化开单.py @@ -0,0 +1,302 @@ +import os +import time +import uuid +from dataclasses import dataclass +from typing import Optional + +import requests +from loguru import logger +from bitmart.api_contract import APIContract +from bitmart.lib.cloud_exceptions import APIException + +from DrissionPage import ChromiumPage, ChromiumOptions + + +# ========= 1) 配置集中 ========= +@dataclass +class TradeConfig: + contract_symbol: str = "ETHUSDT" + leverage: str = "100" + open_type: str = "cross" # 全仓 + risk_percent: float = 0.01 # 用余额的1% + min_size: int = 1 + max_size: int = 5000 + + # TGE + tge_url: str = "http://127.0.0.1:50326" + tge_id: int = 196495 + tge_token: str = os.getenv("TGE_TOKEN", "") + tge_auth_header: str = "Authorization" + + +# ========= 2) API 封装(查询 + 平仓)========= +class BitmartApi: + def __init__(self, cfg: TradeConfig): + api_key = "a0fb7b98464fd9bcce67e7c519d58ec10d0c38a8" + secret_key = "4eaeba78e77aeaab1c2027f846a276d164f264a44c2c1bb1c5f3be50c8de1ca5" + memo = "合约交易" + + if not api_key or not secret_key: + raise RuntimeError("请设置环境变量 BITMART_API_KEY / BITMART_SECRET_KEY") + + self.cfg = cfg + self.contractAPI = APIContract(api_key, secret_key, memo, timeout=(5, 15)) + + def set_leverage(self) -> bool: + try: + resp = self.contractAPI.post_submit_leverage( + contract_symbol=self.cfg.contract_symbol, + leverage=self.cfg.leverage, + open_type=self.cfg.open_type + )[0] + if resp.get("code") == 1000: + logger.success(f"杠杆设置成功:{self.cfg.open_type} + {self.cfg.leverage}x") + return True + logger.error(f"杠杆设置失败: {resp}") + return False + except Exception as e: + logger.exception(e) + return False + + def get_available_balance(self) -> float: + try: + resp = self.contractAPI.get_assets_detail()[0] + if resp.get("code") != 1000: + return 0.0 + data = resp.get("data") + if isinstance(data, dict): + return float(data.get("available_balance", 0)) + if isinstance(data, list): + for a in data: + if a.get("currency") == "USDT": + return float(a.get("available_balance", 0)) + return 0.0 + except Exception as e: + logger.exception(e) + return 0.0 + + def get_last_price(self) -> Optional[float]: + """尽量用 ticker/contract_details;不行再退化到 kline""" + try: + if hasattr(self.contractAPI, "get_contract_details"): + r = self.contractAPI.get_contract_details(contract_symbol=self.cfg.contract_symbol)[0] + d = r.get("data") + if isinstance(d, dict): + for k in ("last_price", "mark_price", "index_price"): + if d.get(k) is not None: + return float(d[k]) + except Exception: + pass + return None + + def get_position(self) -> dict: + """返回原始持仓信息(你策略里爱怎么用怎么用)""" + try: + resp = self.contractAPI.get_position(contract_symbol=self.cfg.contract_symbol)[0] + if resp.get("code") != 1000: + return {} + data = resp.get("data") or [] + return data[0] if data else {} + except Exception as e: + logger.exception(e) + return {} + + def close_all_market(self) -> bool: + """用 API 市价一键平仓(比网页稳)""" + p = self.get_position() + if not p: + return True + + pos_type = p.get("position_type") # 1=多 2=空(按你账户实际字段确认) + if pos_type == 1: + side = 3 # 平多(你原逻辑) + else: + side = 2 # 平空(你原逻辑) + + try: + client_order_id = f"close_{int(time.time())}_{uuid.uuid4().hex[:6]}" + 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=999999 + )[0] + ok = (resp.get("code") == 1000) + if not ok: + logger.error(f"平仓失败: {resp}") + return ok + except APIException as e: + logger.error(f"平仓API异常: {e}") + return False + except Exception as e: + logger.exception(e) + return False + + def calc_size(self, price: float) -> int: + bal = self.get_available_balance() + if bal <= 0 or price <= 0: + return 0 + margin = bal * self.cfg.risk_percent + lev = int(self.cfg.leverage) + + # ⚠️ 你原来假设:1张≈0.001ETH(按你实际合约面值调整) + size = int((margin * lev) / (price * 0.001)) + size = max(self.cfg.min_size, size) + size = min(self.cfg.max_size, size) + return size + + +# ========= 3) 网页下单封装(只负责“开仓”)========= +class BitmartWebTrader: + class X: + MARKET_BTN = 'x://button[normalize-space(text()) ="市价"]' + LIMIT_BTN = 'x://button[normalize-space(text()) ="限价"]' + SIZE_INPUT = 'x://*[@id="size_0"]' + PRICE_INPUT= 'x://*[@id="price_0"]' + BUY_LONG = 'x://span[normalize-space(text()) ="买入/做多"]' + SELL_SHORT = 'x://span[normalize-space(text()) ="卖出/做空"]' + + def __init__(self, cfg: TradeConfig): + self.cfg = cfg + self.page: Optional[ChromiumPage] = None + self._tge_port: Optional[int] = None + + def _post_with_retry(self, url, json_data, headers, retry=3, sleep=0.6): + last = None + for i in range(retry): + try: + r = requests.post(url, json=json_data, headers=headers, timeout=(5, 15)) + r.raise_for_status() + return r.json() + except Exception as e: + last = e + time.sleep(sleep) + raise last + + def open_and_takeover(self) -> None: + headers = { + self.cfg.tge_auth_header: f"Bearer {self.cfg.tge_token}", + "Content-Type": "application/json", + } + + j = self._post_with_retry( + f"{self.cfg.tge_url}/api/browser/start", + {"envId": self.cfg.tge_id}, + headers=headers + ) + self._tge_port = j["data"]["port"] + + co = ChromiumOptions() + co.set_local_port(self._tge_port) + self.page = ChromiumPage(addr_or_opts=co) + self.page.set.window.max() + + def _ele(self, xpath: str, timeout=8): + """统一等待元素出现(按 DrissionPage 版本可微调)""" + t0 = time.time() + while time.time() - t0 < timeout: + ele = self.page.ele(xpath) + if ele: + return ele + time.sleep(0.2) + return None + + def _click(self, xpath: str, timeout=8, sleep=0.2) -> bool: + ele = self._ele(xpath, timeout=timeout) + if not ele: + logger.error(f"找不到元素: {xpath}") + return False + try: + ele.scroll.to_see(center=True) + time.sleep(sleep) + ele.click() + return True + except Exception as e: + logger.error(f"点击失败 {xpath}: {e}") + return False + + def _input(self, xpath: str, value, clear=True, timeout=8) -> bool: + ele = self._ele(xpath, timeout=timeout) + if not ele: + logger.error(f"找不到输入框: {xpath}") + return False + try: + if clear: + ele.input(vals="", clear=True) + time.sleep(0.1) + ele.input(value) + return True + except Exception as e: + logger.error(f"输入失败 {xpath}: {e}") + return False + + def open_market_long(self, size: int) -> bool: + return ( + self._click(self.X.MARKET_BTN) and + self._input(self.X.SIZE_INPUT, size, clear=True) and + self._click(self.X.BUY_LONG) + ) + + def open_market_short(self, size: int) -> bool: + return ( + self._click(self.X.MARKET_BTN) and + self._input(self.X.SIZE_INPUT, size, clear=True) and + self._click(self.X.SELL_SHORT) + ) + + +# ========= 4) 路由:开仓网页,平仓/查询API ========= +class OrderRouter: + def __init__(self, api: BitmartApi, web: BitmartWebTrader): + self.api = api + self.web = web + + def bootstrap(self): + if not self.api.set_leverage(): + raise RuntimeError("杠杆设置失败") + self.web.open_and_takeover() + + def open_long(self) -> bool: + price = self.api.get_last_price() + if not price: + logger.error("拿不到价格,取消开仓") + return False + size = self.api.calc_size(price) + if size <= 0: + logger.error("size=0,取消开仓") + return False + return self.web.open_market_long(size) + + def open_short(self) -> bool: + price = self.api.get_last_price() + if not price: + logger.error("拿不到价格,取消开仓") + return False + size = self.api.calc_size(price) + if size <= 0: + logger.error("size=0,取消开仓") + return False + return self.web.open_market_short(size) + + def close_all(self) -> bool: + return self.api.close_all_market() + + +if __name__ == "__main__": + cfg = TradeConfig(tge_id=196495) + api = BitmartApi(cfg) + web = BitmartWebTrader(cfg) + router = OrderRouter(api, web) + + router.bootstrap() + + # 示例:开多 -> 5秒后平 + ok = router.open_long() + logger.info(f"开多结果: {ok}") + time.sleep(5) + ok2 = router.close_all() + logger.info(f"平仓结果: {ok2}") diff --git a/bitmart/框架.py b/bitmart/框架.py index 25d6466..dcae413 100644 --- a/bitmart/框架.py +++ b/bitmart/框架.py @@ -248,16 +248,18 @@ class BitmartFuturesTransaction: return logger.info("浏览器接管成功") - self.close_extra_tabs() - self.page.get("https://derivatives.bitmart.com/zh-CN/futures/ETHUSDT") + self.get_klines() - self.click_safe('x://button[normalize-space(text()) ="市价"]') - self.click_safe('x://button[normalize-space(text()) ="限价"]') - - self.page.ele('x://*[@id="price_0"]').input(vals=3000, clear=True) - self.page.ele('x://*[@id="size_0"]').input(1) - self.click_safe('x://span[normalize-space(text()) ="买入/做多"]') - self.click_safe('x://span[normalize-space(text()) ="卖出/做空"]') + # self.close_extra_tabs() + # self.page.get("https://derivatives.bitmart.com/zh-CN/futures/ETHUSDT") + # + # self.click_safe('x://button[normalize-space(text()) ="市价"]') + # self.click_safe('x://button[normalize-space(text()) ="限价"]') + # + # self.page.ele('x://*[@id="price_0"]').input(vals=3000, clear=True) + # self.page.ele('x://*[@id="size_0"]').input(1) + # self.click_safe('x://span[normalize-space(text()) ="买入/做多"]') + # self.click_safe('x://span[normalize-space(text()) ="卖出/做空"]') if __name__ == '__main__': diff --git a/telegram/8619211027341.session b/telegram/8619211027341.session index 1002c3b18b207030295c4824e769ad915cae16f7..bf65e6b6041581005d7ca607b5da92f5c14408e6 100644 GIT binary patch delta 75 zcmZp8z|`=7X@V4!me@oYCm^{oVM#8RoS5USptsXjo7|qvm#4rUmg&7MOz`+-)4ZcB c%--7~H~%e~$OvRaZJt=7XblpXymh@d03XdAQ2+n{ delta 75 zcmZp8z|`=7X@V4!X6r;5Cm^{oVM#8R>^a99=7}YW)*z9|Ti1I703W>^HUIzs diff --git a/telegram/bot_session.session b/telegram/bot_session.session index 674f6751ed0c24a9f9a00b12ded7be8b1685bc53..7989bdabc7150da66d134e3854cd2d66e6fae31d 100644 GIT binary patch delta 4087 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 bgo5lk0yLM81#9Zof}hJ_lZHm*TVwwNxIFl- delta 1515 zcmXX`2~1R16#f6dzpp}25sD5f>p&TBp)v(UQWaDLY16s@qBe-J(nip61!TvH3>Os; zwN+U(h=Gq-cA^v;tf19M3&n*N5sE0(y403*82gf!b93IiFS+NPdlM5;;zJ_GhThtM zVVGdl_z_zhDmR?-n1qym6N3rLD`iCKmo}?bx*rtKpXY2%EMN4ju8%VX+==XpmMNxy zJ@)!)rc2eLWDgtg?_Pbh{qKl43yj8ak|btmjlIq>L1D#kGIm9R-A&WEWld}OaZ{RP z+V7U6v|($oRtyy=dCD3^mb>Na@=5#;d8fQkw!&JaCsMU!lr~G=N~ffVzl+skmKY|w zi@4AslnWWcCLutu;-B;N{MY;jem-x--6w;&O41ph%I)RCI2U%5Z6zYt)0?^OzuWI zQ7D>=Bw~=bN)+JH*gx1EtRJ&Iei2Idy8xHs3R?_r&>24)E+3f*4|Z;Z&kp!PSc*6J z@A3g>Zx{IOU>7tcEJQ(})$laV0o-;tpdfcY$TK*?fI$m&$-(e=z6Mr$xY|aKGlNG*Aq~}cjB_3Hk0DqXw~qUekj=InIPegSR^)vDNRbgvRw&K?BrMSE%~gR zB(IYf$djZ#sZlDD_DX9dy);=I72CyPFb@E-qy@8FN|20n!M z;uUU~YvT&IueimW9s82)V9S^XOdXShFJ)pGJ!3^)P-u05yhb8$Q{YV2vJWI5_^a>!~((wKZ6h9?YN^c z$rXgu8IW8!nN;p$_K=lp2l{k3(xtGm86Ku+VQZ1I#>Q+}#^g=RmW&*Ycor>6yVu6X zZ`u-Ni1~=y4L(IWHMghfnVj8tK&`&>I6Gzwld# zJP{7QY2y%`?u3GtO;e5O9D)4pN%dqVg`SIkEvw}m~G5T#)Gk-d+2j? zGM+-Or~OC+t)X5~UDOro7-c|@p(w{XL4GTDWB23=IY<6dUM1^f1@DnYq)w?+O2;or z(Zn3d&)h}zB8brhA^M1v@RLv>q@mTgmk=(@6BK@czs?)^Xx@jnBtyAJ+!eGBxssRh za8ko1a-VRn?7!@7_QL-SYzB{_e-r6ME;9ANcFKc_;Qgp;yftlLddWO)g!Sgy zS)ME6A~`KP&fZ|7sEo>_IHZoB9mLIL4i#(7XkFrHM*otzW;B%iIDzWY(y8y<6jk=t xG&sB<@_dcCDC-MhCO^ii+b-I;NB&e7aOch{X|3Cg?-GiPSpnW?ryte5OXDTo3$Z>_5<8VUxY z#zOc~o2PPqINnwSyi8E_%IGVf#QcJ4q$j&-A?g4)vCm?Jwzis*72G zv-TK>v1zuFehzw=75gW44y2h8DrW2}^HWD$Gc%jt>{h}{`sqk0+!xkVwG;_RX%JBZ zl0jl2xzTOPG(q??(pxEw940kJPNT!cy_L<(ab>KbS9ygNOL{V2XT?v>yOlM=x+Kbb(9BxT6 h1r||~HRZo0jNLald@0FPZBG#-Nl|kaV>~fQ{RMjZ-FN^1 delta 479 zcmZoTz|?SnX@WFk+C&*=#&#{naG5ZBx z6P|+{I-D!`XYsV~webGn73bK;m(F>d=N|8Tz8{=rn_UDH*(Xl$pS(ugl2K&xGjUfS zsUxvWm~$F~=)wXv26+w!j^gx`#BxKU$xkJ&bITbS7@6uC8tEEZY(6gefrU|S@(#IG z+yQW5v&p&g3EU|#zLD|d8}eFOoeTneZcO|y`7iPB=3l};fxn7Bh3^r+AHO0$H@`Wb z8(%SBEZ=D$j@@i%AjLP?O;0<@$Vfzzg;kN0fpHqg+$C@Wjre{rIDte{rcC&N5ET)J zs6`Pq1gg~msonc^>wdUe!_BAl%ow%#mI57>%)mdDub=NC|7(7Aep~*P{F(gw`Tp`f z=db4v=1b