Files
lm_code/weex/框架.py
Administrator fd63961e3c haha
2026-02-03 18:26:58 +08:00

328 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
WEEX ETH-USDT 永续合约交易框架
与 bitmart/框架.py 结构对应:浏览器 + APIK线/余额/持仓)+ 市价开平仓,供具体策略复用。
"""
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()