bitmart优化完成
This commit is contained in:
@@ -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秒超时
|
||||
458
bitmart/优化拿仓版本.py
Normal file
458
bitmart/优化拿仓版本.py
Normal file
@@ -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()
|
||||
866
bitmart/均线回归.py
Normal file
866
bitmart/均线回归.py
Normal file
@@ -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
|
||||
|
||||
# 目前动态计算阀值的速度是多少
|
||||
302
bitmart/均线自动化开单.py
Normal file
302
bitmart/均线自动化开单.py
Normal file
@@ -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}")
|
||||
@@ -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__':
|
||||
|
||||
Binary file not shown.
Binary file not shown.
BIN
telegram/sign.db
BIN
telegram/sign.db
Binary file not shown.
Reference in New Issue
Block a user