import time import datetime from tqdm import tqdm from loguru import logger from DrissionPage import ChromiumOptions, ChromiumPage from curl_cffi import requests from 交易.tools import send_dingtalk_message def is_bullish(c): return float(c['close']) > float(c['open']) def is_bearish(c): return float(c['close']) < float(c['open']) class WeexTransaction: """BitMart 自动交易脚本(优化版)""" def __init__(self, tge_id): self.tge_url = "http://127.0.0.1:50326" self.tge_id = tge_id self.tge_headers = { "Authorization": "Bearer asp_174003986c9b0799677c5b2c1adb76e402735d753bc91a91", "Content-Type": "application/json" } self.page: ChromiumPage | None = None self.session = requests.Session() self.cookies = {} self.tge_port = None # 当前仓位(-1 空 / 0 无 / 1 多) self.start = 0 # 最新 3 根 kline self.kline_1 = None self.kline_2 = None self.kline_3 = None # 当前信号 self.direction = None # 防止同一时段重复执行 self.time_start = None self.pbar = None # ------------------------------------------------------------- # ------------------------- 通用工具 ---------------------------- # ------------------------------------------------------------- def now_text(self): """格式化当前时间""" return datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") def ding(self, msg, error=False): """统一钉钉消息格式""" prefix = "❌bitmart:" if error else "🔔bitmart:" send_dingtalk_message(f"{prefix}{self.now_text()},{msg}") def get_half_hour_timestamp(self): """返回当前最近的整点或半点时间戳""" ts = time.time() dt = datetime.datetime.fromtimestamp(ts) target = dt.replace(minute=0, second=0, microsecond=0) if dt.minute < 30 \ else dt.replace(minute=30, second=0, microsecond=0) return int(target.timestamp()) # ------------------------------------------------------------- # ---------------------- TGE 浏览器相关 ------------------------ # ------------------------------------------------------------- def openBrowser(self): """打开 TGE 对应浏览器实例""" try: res = requests.post( f"{self.tge_url}/api/browser/start", json={"envId": self.tge_id}, headers=self.tge_headers ) self.tge_port = res.json()["data"]["port"] 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 # ------------------------------------------------------------- # -------------------- BitMart Token & 接口 -------------------- # ------------------------------------------------------------- def get_token(self): """从浏览器监听接口中提取 token 和 cookies""" tab = self.page.new_tab() tab.listen.start("ifcontract/shareguide/status") for _ in range(3): try: tab.get("https://derivatives.bitmart.com/zh-CN/futures/ETHUSDT") res = tab.listen.wait(timeout=15) # 请求头 + Cookies for i in res.request.headers: if ":" not in i: self.session.headers[i] = res.request.headers[i] for c in res.request.cookies: self.cookies[c["name"]] = c["value"] if "accessKey" in self.cookies: self.session.cookies.update(self.cookies) tab.close() return True except: time.sleep(1) tab.close() return False def get_account_balance(self): """获取可用余额""" params = {'account_type': '3', 'isServerCal': '1'} for _ in range(3): try: res = self.session.get( 'https://derivatives.bitmart.com/gw-api/contract-tiger/forward/v1/ifcontract/accounts', params=params ) return float(res.json()["data"]["accounts"][0]["available_balance"]) except: time.sleep(1) return None def get_position_status(self): """获取当前仓位:1 多、-1 空、0 无""" params = {'status': '1'} for _ in range(3): try: res = self.session.get( 'https://derivatives.bitmart.com/gw-api/contract-tiger/forward/v1/ifcontract/userPositions', params=params ).json() pos = res.get("data", {}).get("positions", []) if not pos: self.start = 0 else: ptype = pos[0].get("position_type") self.start = 1 if ptype == 1 else -1 return True except: time.sleep(1) return False # ------------------------------------------------------------- # --------------------------- K线相关 --------------------------- # ------------------------------------------------------------- def get_price(self): """获取最新 15 分钟 K 线""" params = { 'unit': '30', 'resolution': 'M', 'contractID': '2', 'offset': '1', 'endTime': str(int(time.time())), } for _ in range(3): try: res = self.session.get( 'https://contract-v2.bitmart.com/v1/ifcontract/quote/kline', params=params, timeout=15 ) datas = [] for k in res.json()["data"]: datas.append({ 'id': int(k["timestamp"]) - 1, 'open': float(k["open"]), 'high': float(k["high"]), 'low': float(k["low"]), 'close': float(k["close"]) }) return sorted(datas, key=lambda x: x["id"]) except: time.sleep(1) return None def check_signal(self, prev, curr): """包住形态判定""" p_open, p_close = prev["open"], prev["close"] c_open, c_close = curr["open"], curr["close"] # 前跌后涨包住 -> 多 if is_bullish(curr) and is_bearish(prev) and c_close >= p_open: return "long" # 前涨后跌包住 -> 空 if is_bearish(curr) and is_bullish(prev) and c_close <= p_open: return "short" return None # ------------------------------------------------------------- # ---------------------- 下单逻辑封装 --------------------------- # ------------------------------------------------------------- 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 do_order(self): """执行交易动作(开仓/反手)""" # 选择市价 self.click_safe('x:(//button[normalize-space(text())="市价"])') # 余额 balance = None for i in range(3): balance = self.get_account_balance() if not balance: self.ding("获取可用余额失败!", error=True) return amount = balance / 100 self.page.ele('x://*[@id="size_0"]').input(amount) # ---- 开仓逻辑 ---- if self.direction == "long": if self.start == 0: self.ding(f"信号:多,开多:{amount}") self.click_safe('x://span[normalize-space(text()) ="买入/做多"]') self.start = 1 elif self.start == -1: self.ding(f"信号:多,反手空转多:{amount}") self.click_safe('x://span[normalize-space(text()) ="市价"]') time.sleep(2) self.click_safe('x://span[normalize-space(text()) ="买入/做多"]') self.start = 1 elif self.direction == "short": if self.start == 0: self.ding(f"信号:空,开空:{amount}") self.click_safe('x://span[normalize-space(text()) ="卖出/做空"]') self.start = -1 elif self.start == 1: self.ding(f"信号:空,反手多转空:{amount}") self.click_safe('x://span[normalize-space(text()) ="市价"]') time.sleep(2) self.click_safe('x://span[normalize-space(text()) ="卖出/做空"]') self.start = -1 # ------------------------------------------------------------- # ----------------------------- 主流程 --------------------------- # ------------------------------------------------------------- def action(self): # 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.close_extra_tabs() self.page.get("https://derivatives.bitmart.com/zh-CN/futures/ETHUSDT") self.pbar = tqdm(total=30, desc="等待半小时周期", ncols=80) while True: now = time.localtime() minute = now.tm_min # 更新进度条 self.pbar.n = minute if minute < 30 else minute - 30 self.pbar.refresh() # 必须是整点或半点及前 5 分钟 # if minute not in [0, 1, 2, 3, 4, 5, 30, 31, 32, 33, 34, 35]: # time.sleep(8) # return # 时间重复跳过 if self.time_start == self.get_half_hour_timestamp(): time.sleep(5) continue # ---- 获取 Token ---- if not self.get_token(): self.ding("获取 token 失败!", error=True) return logger.info("Token 获取成功") # ---- 获取价格 ---- kdatas = self.get_price() if not kdatas: self.ding("获取价格失败!", error=True) return self.kline_1, self.kline_2, self.kline_3 = kdatas[-3:] if int(self.kline_3["id"]) != self.get_half_hour_timestamp(): continue logger.success("K线获取成功") self.time_start = self.get_half_hour_timestamp() # ---- 刷新页面 ---- self.page.get("https://derivatives.bitmart.com/zh-CN/futures/ETHUSDT") for i in range(3): # ---- 获取仓位 ---- if not self.get_position_status(): self.ding("获取仓位失败!", error=True) continue if self.start: break # ---- 止损平仓 ---- try: if self.start == 1 and is_bearish(self.kline_1) and is_bearish(self.kline_2): self.ding("两根大阴线,平多") self.click_safe('x://span[normalize-space(text()) ="市价"]') self.start = 0 elif self.start == -1 and is_bullish(self.kline_1) and is_bullish(self.kline_2): self.ding("两根大阳线,平空") self.click_safe('x://span[normalize-space(text()) ="市价"]') self.start = 0 except: self.ding("止损平仓错误!", error=True) # continue return # ---- 生成新信号 ---- self.direction = self.check_signal(prev=self.kline_1, curr=self.kline_2) # ---- 执行交易 ---- if self.direction: try: self.do_order() except: self.ding("下单失败!", error=True) # ---- 周期结束消息 ---- self.pbar.reset() self.ding( f"持仓:{'无' if self.start == 0 else ('多' if self.start == 1 else '空')}," f"信号:{'无' if not self.direction else ('多' if self.direction == 'long' else '空')}" ) if __name__ == '__main__': while True: try: WeexTransaction(tge_id=196495).action() except: time.sleep(5)