2025-12-12 17:17:56 +08:00
|
|
|
|
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()
|
2025-12-13 14:05:50 +08:00
|
|
|
|
tab.listen.start("ifcontract/shareguide/status")
|
2025-12-12 17:17:56 +08:00
|
|
|
|
|
|
|
|
|
|
for _ in range(3):
|
|
|
|
|
|
try:
|
|
|
|
|
|
tab.get("https://derivatives.bitmart.com/zh-CN/futures/ETHUSDT")
|
2025-12-15 16:56:35 +08:00
|
|
|
|
res = tab.listen.wait(timeout=15)
|
2025-12-12 17:17:56 +08:00
|
|
|
|
|
|
|
|
|
|
# 请求头 + Cookies
|
2025-12-15 16:56:35 +08:00
|
|
|
|
for i in res.request.headers:
|
|
|
|
|
|
if ":" not in i:
|
|
|
|
|
|
self.session.headers[i] = res.request.headers[i]
|
2025-12-12 17:17:56 +08:00
|
|
|
|
|
|
|
|
|
|
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',
|
2025-12-16 10:13:00 +08:00
|
|
|
|
'offset': '1',
|
2025-12-12 17:17:56 +08:00
|
|
|
|
'endTime': str(int(time.time())),
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
for _ in range(3):
|
|
|
|
|
|
try:
|
|
|
|
|
|
res = self.session.get(
|
|
|
|
|
|
'https://contract-v2.bitmart.com/v1/ifcontract/quote/kline',
|
2025-12-16 10:13:00 +08:00
|
|
|
|
params=params, timeout=15
|
2025-12-12 17:17:56 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
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())="市价"])')
|
|
|
|
|
|
|
|
|
|
|
|
# 余额
|
2025-12-15 23:04:32 +08:00
|
|
|
|
balance = None
|
|
|
|
|
|
for i in range(3):
|
|
|
|
|
|
balance = self.get_account_balance()
|
|
|
|
|
|
if not balance:
|
|
|
|
|
|
self.ding("获取可用余额失败!", error=True)
|
|
|
|
|
|
return
|
2025-12-12 17:17:56 +08:00
|
|
|
|
|
|
|
|
|
|
amount = balance / 100
|
|
|
|
|
|
self.page.ele('x://*[@id="size_0"]').input(amount)
|
|
|
|
|
|
|
|
|
|
|
|
# ---- 开仓逻辑 ----
|
|
|
|
|
|
if self.direction == "long":
|
|
|
|
|
|
if self.start == 0:
|
|
|
|
|
|
self.ding(f"信号:多,开多:{amount}")
|
2025-12-12 18:09:09 +08:00
|
|
|
|
self.click_safe('x://span[normalize-space(text()) ="买入/做多"]')
|
2025-12-12 17:17:56 +08:00
|
|
|
|
self.start = 1
|
|
|
|
|
|
elif self.start == -1:
|
|
|
|
|
|
self.ding(f"信号:多,反手空转多:{amount}")
|
2025-12-12 18:09:09 +08:00
|
|
|
|
self.click_safe('x://span[normalize-space(text()) ="市价"]')
|
2025-12-12 17:17:56 +08:00
|
|
|
|
time.sleep(2)
|
2025-12-12 18:09:09 +08:00
|
|
|
|
self.click_safe('x://span[normalize-space(text()) ="买入/做多"]')
|
2025-12-12 17:17:56 +08:00
|
|
|
|
self.start = 1
|
|
|
|
|
|
|
|
|
|
|
|
elif self.direction == "short":
|
|
|
|
|
|
if self.start == 0:
|
|
|
|
|
|
self.ding(f"信号:空,开空:{amount}")
|
2025-12-12 18:09:09 +08:00
|
|
|
|
self.click_safe('x://span[normalize-space(text()) ="卖出/做空"]')
|
2025-12-12 17:17:56 +08:00
|
|
|
|
self.start = -1
|
|
|
|
|
|
elif self.start == 1:
|
|
|
|
|
|
self.ding(f"信号:空,反手多转空:{amount}")
|
2025-12-12 18:09:09 +08:00
|
|
|
|
self.click_safe('x://span[normalize-space(text()) ="市价"]')
|
2025-12-12 17:17:56 +08:00
|
|
|
|
time.sleep(2)
|
2025-12-12 18:09:09 +08:00
|
|
|
|
self.click_safe('x://span[normalize-space(text()) ="卖出/做空"]')
|
2025-12-12 17:17:56 +08:00
|
|
|
|
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)
|
|
|
|
|
|
|
2025-12-15 16:56:35 +08:00
|
|
|
|
while True:
|
|
|
|
|
|
now = time.localtime()
|
|
|
|
|
|
minute = now.tm_min
|
2025-12-12 17:17:56 +08:00
|
|
|
|
|
2025-12-15 16:56:35 +08:00
|
|
|
|
# 更新进度条
|
|
|
|
|
|
self.pbar.n = minute if minute < 30 else minute - 30
|
|
|
|
|
|
self.pbar.refresh()
|
2025-12-12 17:17:56 +08:00
|
|
|
|
|
2025-12-15 23:04:32 +08:00
|
|
|
|
# 必须是整点或半点及前 5 分钟
|
2025-12-16 10:13:00 +08:00
|
|
|
|
# if minute not in [0, 1, 2, 3, 4, 5, 30, 31, 32, 33, 34, 35]:
|
|
|
|
|
|
# time.sleep(8)
|
|
|
|
|
|
# return
|
2025-12-12 17:17:56 +08:00
|
|
|
|
|
2025-12-15 16:56:35 +08:00
|
|
|
|
# 时间重复跳过
|
|
|
|
|
|
if self.time_start == self.get_half_hour_timestamp():
|
|
|
|
|
|
time.sleep(5)
|
|
|
|
|
|
continue
|
2025-12-12 17:17:56 +08:00
|
|
|
|
|
2025-12-15 16:56:35 +08:00
|
|
|
|
# ---- 获取 Token ----
|
|
|
|
|
|
if not self.get_token():
|
|
|
|
|
|
self.ding("获取 token 失败!", error=True)
|
|
|
|
|
|
return
|
|
|
|
|
|
logger.info("Token 获取成功")
|
2025-12-12 17:17:56 +08:00
|
|
|
|
|
2025-12-15 16:56:35 +08:00
|
|
|
|
# ---- 获取价格 ----
|
|
|
|
|
|
kdatas = self.get_price()
|
|
|
|
|
|
if not kdatas:
|
|
|
|
|
|
self.ding("获取价格失败!", error=True)
|
|
|
|
|
|
return
|
2025-12-12 17:17:56 +08:00
|
|
|
|
|
2025-12-15 16:56:35 +08:00
|
|
|
|
self.kline_1, self.kline_2, self.kline_3 = kdatas[-3:]
|
|
|
|
|
|
if int(self.kline_3["id"]) != self.get_half_hour_timestamp():
|
2025-12-15 15:56:42 +08:00
|
|
|
|
continue
|
2025-12-13 05:37:38 +08:00
|
|
|
|
|
2025-12-15 16:56:35 +08:00
|
|
|
|
logger.success("K线获取成功")
|
2025-12-12 17:17:56 +08:00
|
|
|
|
|
2025-12-15 16:56:35 +08:00
|
|
|
|
self.time_start = self.get_half_hour_timestamp()
|
2025-12-12 17:17:56 +08:00
|
|
|
|
|
2025-12-15 16:56:35 +08:00
|
|
|
|
# ---- 刷新页面 ----
|
|
|
|
|
|
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
|
2025-12-15 15:56:42 +08:00
|
|
|
|
|
2025-12-15 16:56:35 +08:00
|
|
|
|
if self.start:
|
|
|
|
|
|
break
|
2025-12-15 15:56:42 +08:00
|
|
|
|
|
2025-12-15 16:56:35 +08:00
|
|
|
|
# ---- 止损平仓 ----
|
2025-12-15 15:56:42 +08:00
|
|
|
|
try:
|
2025-12-15 16:56:35 +08:00
|
|
|
|
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
|
2025-12-12 17:17:56 +08:00
|
|
|
|
|
2025-12-15 16:56:35 +08:00
|
|
|
|
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 '空')}"
|
|
|
|
|
|
)
|
2025-12-12 17:17:56 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
2025-12-16 10:13:00 +08:00
|
|
|
|
while True:
|
|
|
|
|
|
try:
|
|
|
|
|
|
WeexTransaction(tge_id=196495).action()
|
|
|
|
|
|
except:
|
|
|
|
|
|
time.sleep(5)
|