""" WEEX ETH-USDT 永续合约交易框架 与 bitmart/框架.py 结构对应:浏览器 + API(K线/余额/持仓)+ 市价开平仓,供具体策略复用。 """ import time from typing import Optional, Dict, List, Tuple from tqdm import tqdm from loguru import logger from bit_tools import openBrowser from DrissionPage import ChromiumPage, ChromiumOptions from curl_cffi import requests # ==================== 配置 ==================== class Config: TGE_URL = "http://127.0.0.1:50326" TGE_AUTHORIZATION = "Bearer asp_174003986c9b0799677c5b2c1adb76e402735d753bc91a91" CONTRACT_ID = "10000002" PRODUCT_CODE = "cmt_ethusdt" KLINE_TYPE = "MINUTE_30" KLINE_LIMIT = 300 TRADING_URL = "https://www.weex.com/zh-CN/futures/ETH-USDT" MAX_RETRY_ATTEMPTS = 3 RETRY_DELAY = 1 # ==================== 主框架类 ==================== class WeexFuturesTransaction: """WEEX 永续合约交易框架:K线、余额、持仓、浏览器、市价开平仓""" def __init__(self, tge_id): self.page: Optional[ChromiumPage] = None self.tge_port: Optional[int] = None self.tge_id = tge_id self.session = requests.Session() self.headers: Optional[Dict] = None self.start = 0 # 持仓: -1 空, 0 无, 1 多 self.open_avg_price = None self.current_amount = None self.position_data: Optional[List] = None self.pbar = tqdm(total=30, desc="等待K线", ncols=80) self.last_kline_time = None # ------------------------- Token(请求 API 前需先取 token)------------------------- def _get_token(self) -> bool: """从浏览器请求交易页时抓取 U-TOKEN,写入 session headers""" if not self.page: return False tab = self.page.new_tab() tab.listen.start("/user/security/getLanguageType") try: for attempt in range(Config.MAX_RETRY_ATTEMPTS): try: tab.get(url=Config.TRADING_URL) res = tab.listen.wait(timeout=5) if res.request.headers.get("U-TOKEN"): self.headers = dict(res.request.headers) self.session.headers.update(self.headers) logger.success("获取 token 成功") return True except Exception as e: logger.warning(f"获取 token 第 {attempt + 1} 次失败: {e}") if attempt < Config.MAX_RETRY_ATTEMPTS - 1: time.sleep(Config.RETRY_DELAY) return False finally: tab.close() # ------------------------- API ------------------------- def get_klines( self, contract_id: str = Config.CONTRACT_ID, product_code: str = Config.PRODUCT_CODE, kline_type: str = Config.KLINE_TYPE, limit: int = Config.KLINE_LIMIT, ) -> Optional[List[Dict]]: """获取 K 线,返回 [{'id', 'open', 'high', 'low', 'close'}, ...],按 id 升序。请求前需已取 token。""" # if not self._get_token(): # logger.error("获取 token 失败,无法拉取 K 线") # return None params = { "contractId": contract_id, "productCode": product_code, "priceType": "LAST_PRICE", "klineType": kline_type, "limit": str(limit), "timeZone": "string", "languageType": "1", "sign": "SIGN", } for attempt in range(Config.MAX_RETRY_ATTEMPTS): try: response = self.session.get( "https://http-gateway2.elconvo.com/api/v1/public/quote/v1/getKlineV2", params=params, timeout=15, ) if response.status_code != 200: if attempt < Config.MAX_RETRY_ATTEMPTS - 1: time.sleep(Config.RETRY_DELAY) continue data = response.json() if "data" not in data or "dataList" not in data["data"]: continue result = data["data"]["dataList"] kline_data = [] for item in result: # [close, high, low, open, id] kline_data.append({ "id": int(item[4]), "open": float(item[3]), "high": float(item[1]), "low": float(item[2]), "close": float(item[0]), }) kline_data.sort(key=lambda x: x["id"]) return kline_data except Exception as e: logger.error(f"获取 K 线异常: {e}") if attempt < Config.MAX_RETRY_ATTEMPTS - 1: time.sleep(Config.RETRY_DELAY) return None def get_current_price(self) -> Optional[float]: """用最近一根 K 线收盘价作为当前价""" klines = self.get_klines(limit=3) if klines: return float(klines[-1]["close"]) return None def get_available_balance(self) -> Optional[float]: """合约账户可用余额""" if not self._get_token(): return None for attempt in range(Config.MAX_RETRY_ATTEMPTS): try: response = self.session.post( "https://gateway2.ngsvsfx.cn/v1/gw/assetsWithBalance/new", timeout=15, ) return float(response.json()["data"]["newContract"]["balanceList"][0]["available"]) except Exception as e: logger.error(f"获取余额异常: {e}") if attempt < Config.MAX_RETRY_ATTEMPTS - 1: time.sleep(Config.RETRY_DELAY) return None def get_position_status(self) -> bool: """从成交历史解析持仓方向,更新 self.start (-1/0/1),成功返回 True""" # if not self._get_token(): # return False payload = { "filterContractIdList": [10000002], "limit": 100, "languageType": 0, "sign": "SIGN", "timeZone": "string", } for attempt in range(Config.MAX_RETRY_ATTEMPTS): try: response = self.session.post( "https://http-gateway2.janapw.com/api/v1/private/order/v2/getHistoryOrderFillTransactionPage", json=payload, timeout=15, ) datas = response.json().get("data", {}).get("dataList") self.position_data = datas or None if not datas: self.start = 0 return True rev = list(datas) rev.reverse() start, start1 = 0, 0 for i in rev: d = i.get("legacyOrderDirection") if d == "CLOSE_SHORT": start = 0 elif d == "CLOSE_LONG": start1 = 0 elif d == "OPEN_SHORT": start -= 1 elif d == "OPEN_LONG": start1 += 1 if start1: self.start = 1 elif start: self.start = -1 else: self.start = 0 return True except Exception as e: logger.error(f"获取持仓异常: {e}") if attempt < Config.MAX_RETRY_ATTEMPTS - 1: time.sleep(Config.RETRY_DELAY) return False # ------------------------- 浏览器 ------------------------- def openBrowser(self) -> bool: """打开 TGE 对应浏览器实例""" try: bit_port = openBrowser(id=self.tge_id) co = ChromiumOptions() co.set_local_port(port=bit_port) self.page = ChromiumPage(addr_or_opts=co) self.tge_port = bit_port return True except Exception: return False def take_over_browser(self) -> bool: """接管已有浏览器""" if not self.tge_port: return False 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 Exception: return False def close_extra_tabs(self) -> bool: """关闭多余标签页,只保留第一个""" if not self.page: return False try: for idx, tab in enumerate(self.page.get_tabs()): if idx > 0: tab.close() return True except Exception: return False 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(by_js=True) return True except Exception: return False # ------------------------- 交易 ------------------------- def 平仓(self) -> bool: """市价平仓(闪电平仓)""" try: self.page.ele('x:(//span[normalize-space(text()) = "闪电平仓"])').scroll.to_see(center=True) time.sleep(0.5) self.page.ele('x:(//span[normalize-space(text()) = "闪电平仓"])').click(by_js=True) time.sleep(2) logger.info("执行平仓") return True except Exception as e: logger.error(f"平仓异常: {e}") return False def 开单(self, marketPriceLongOrder: int = 0, size: Optional[float] = None) -> bool: """ 市价开仓 marketPriceLongOrder: 1 做多, -1 做空 size: 数量(金额) """ if size is None or size <= 0: logger.warning("开单数量无效") return False try: self.click_safe('x:(//button[normalize-space(text()) = "市价"])') time.sleep(0.5) self.page.ele('x://input[@placeholder="请输入数量"]').input(size) time.sleep(0.5) if marketPriceLongOrder == -1: self.page.ele('x://*[normalize-space(text()) ="卖出开空"]').click(by_js=True) elif marketPriceLongOrder == 1: self.page.ele('x://*[normalize-space(text()) ="买入开多"]').click(by_js=True) else: return False logger.info(f"市价{'做空' if marketPriceLongOrder == -1 else '做多'} 数量={size}") return True except Exception as e: logger.error(f"开单异常: {e}") return False def ding(self, text: str, error: bool = False) -> None: """日志/通知入口,可在此接入钉钉等""" if error: logger.error(text) else: logger.info(text) def action(self) -> None: """框架入口:打开浏览器 -> 交易页 -> 取 token -> 拉 K 线 -> 切市价(示例)""" if not self.openBrowser(): self.ding("打开 TGE 失败!", error=True) return logger.info("TGE 浏览器已打开") self.close_extra_tabs() self.page.get(url=Config.TRADING_URL) time.sleep(2) if not self._get_token(): self.ding("获取 token 失败", error=True) return klines = self.get_klines() if klines: logger.info(f"获取到 {len(klines)} 根 K 线,最新收盘 {klines[-1]['close']}") self.click_safe('x:(//button[normalize-space(text()) = "市价"])') # 此处可接具体策略循环:get_klines -> 信号 -> 开单/平仓 # self.平仓() self.开单(marketPriceLongOrder=1, size=100) if __name__ == "__main__": WeexFuturesTransaction(tge_id="86837a981aba4576be6916a0ef6ad785").action()