bitmart优化完成

This commit is contained in:
Administrator
2025-12-26 23:14:39 +08:00
parent 96f6034340
commit 81ee93878b
5 changed files with 374 additions and 64 deletions

View File

@@ -2,6 +2,7 @@ import time
import uuid
import datetime
import requests
from typing import Tuple
from tqdm import tqdm
from loguru import logger
@@ -107,6 +108,19 @@ class StrategyConfig:
post_sl_mult_max: float = 1.16
post_sl_vol_alpha: float = 0.20
# =========================================================
# ✅ 正常平仓条件价格回归到EMA附近时平仓
# =========================================================
normal_exit_threshold: float = 0.0003 # 0.03%价格回归到EMA附近时平仓
normal_exit_min_profit: float = 0.0002 # 0.02%,正常平仓最小盈利要求
# =========================================================
# ✅ 手续费配置固定10u30倍杠杆
# =========================================================
fixed_margin: float = 10.0 # 固定每单10u保证金
platform_fee_rate: float = 0.0005 # 平台手续费:开仓价值的万分之五
rebate_rate: float = 0.90 # 返佣比例90%
class BitmartFuturesMeanReversionBot:
def __init__(self, cfg: StrategyConfig, bit_id=None):
@@ -132,6 +146,10 @@ class BitmartFuturesMeanReversionBot:
self.entry_ts = None
self.last_exit_ts = 0
# 开仓信息(用于手续费计算)
self.entry_margin = None # 开仓保证金固定10u
self.entry_position_value = None # 开仓时的仓位价值(保证金 * 杠杆)
# 日内权益基准
self.day_start_equity = None
self.trading_enabled = True
@@ -532,20 +550,30 @@ class BitmartFuturesMeanReversionBot:
# ----------------- 下单 -----------------
def calculate_size(self, price: float) -> int:
"""
计算仓位大小
固定每单10u保证金30倍杠杆
"""
bal = self.get_assets_available()
if bal < 10:
if bal < self.cfg.fixed_margin:
logger.warning(f"余额不足:{bal:.2f} USDT < {self.cfg.fixed_margin} USDT")
return 0
margin = bal * self.cfg.risk_percent
# 固定保证金10u
margin = self.cfg.fixed_margin
lev = int(self.cfg.leverage)
# ⚠️ 沿用你的原假设1张≈0.001ETH
# 仓位价值 = 保证金 * 杠杆 = 10 * 30 = 300u
# size = 仓位价值 / (价格 * 0.001)
size = int((margin * lev) / (price * 0.001))
size = max(self.cfg.min_size, size)
size = min(self.cfg.max_size, size)
logger.info(f"计算仓位:保证金={margin}u, 杠杆={lev}x, 仓位价值={margin * lev}u, size={size}")
return size
def place_market_order(self, side: int, size: int, ) -> bool:
def place_market_order(self, side: int, size: int) -> bool:
"""
【下单函数】实际执行下单操作
side: 1=开多, 4=开空, 2=平多, 3=平空
@@ -554,43 +582,221 @@ class BitmartFuturesMeanReversionBot:
return False
try:
# 开多单
if side == 1:
self.click_safe('x://button[normalize-space(text()) ="市价"]')
self.page.ele('x://*[@id="size_0"]').input(size)
self.click_safe('x://span[normalize-space(text()) ="卖出/做空"]')
elif side == 4:
self.click_safe('x://button[normalize-space(text()) ="市价"]')
self.page.ele('x://*[@id="size_0"]').input(size)
self.click_safe('x://button[normalize-space(text()) ="市价"]')
self.page.ele('x://*[@id="size_0"]').input(size, clear=True)
self.click_safe('x://span[normalize-space(text()) ="买入/做多"]')
else:
self.click_safe('x://span[normalize-space(text()) ="市价"]')
# if resp.get("code") == 1000:
logger.info(f"✅ 开多单: size={size}")
return True
# self.ding(f"下单失败: {resp}", error=True)
return False
# 开空单
elif side == 4:
self.click_safe('x://button[normalize-space(text()) ="市价"]')
self.click_safe('x://button[normalize-space(text()) ="市价"]')
self.click_safe('x://button[normalize-space(text()) ="市价"]')
self.page.ele('x://*[@id="size_0"]').input(size, clear=True)
self.click_safe('x://span[normalize-space(text()) ="卖出/做空"]')
logger.info(f"✅ 开空单: size={size}")
return True
except APIException as e:
logger.error(f"API下单异常: {e}")
self.ding(f"API下单异常: {e}", error=True)
return False
# 平多单(平多 = 卖出)
elif side == 2:
self.click_safe('x://button[normalize-space(text()) ="市价"]')
time.sleep(0.3) # 等待界面响应
# 平仓时size可以设置大一些确保全部平仓
self.page.ele('x://*[@id="size_0"]').input(size, clear=True)
time.sleep(0.3)
self.click_safe('x://span[normalize-space(text()) ="卖出/做空"]')
logger.info(f"✅ 平多单操作执行: size={size}")
return True
# 平空单(平空 = 买入)
elif side == 3:
self.click_safe('x://button[normalize-space(text()) ="市价"]')
time.sleep(0.3) # 等待界面响应
self.page.ele('x://*[@id="size_0"]').input(size, clear=True)
time.sleep(0.3)
self.click_safe('x://span[normalize-space(text()) ="买入/做多"]')
logger.info(f"✅ 平空单操作执行: size={size}")
return True
else:
logger.error(f"未知的订单方向: side={side}")
return False
except Exception as e:
logger.error(f"下单未知异常: {e}")
self.ding(f"下单未知异常: {e}", error=True)
logger.error(f"下单异常: {e}")
self.ding(f"下单异常: {e}", error=True)
return False
def close_position_all(self):
def calculate_fee(self, position_value: float) -> float:
"""
计算手续费
平台手续费 = 仓位价值 * 0.0005(万分之五)
实际手续费 = 平台手续费 * (1 - 返佣比例) = 平台手续费 * 0.1
"""
platform_fee = position_value * self.cfg.platform_fee_rate
actual_fee = platform_fee * (1 - self.cfg.rebate_rate)
return actual_fee
def calculate_net_pnl(self, price: float) -> Tuple[float, float, float]:
"""
计算扣除手续费后的净盈亏
返回: (净盈亏比例, 总手续费, 毛盈亏比例)
注意:在杠杆交易中,盈亏比例 = 杠杆倍数 * 价格变动比例
例如30倍杠杆价格涨1%实际盈亏是30%
"""
if self.pos == 0 or self.entry_price is None or self.entry_margin is None or self.entry_position_value is None:
return 0.0, 0.0, 0.0
leverage = int(self.cfg.leverage)
# 计算价格变动比例
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:
price_change_ratio = (price - self.entry_price) / self.entry_price
else:
price_change_ratio = (self.entry_price - price) / self.entry_price
# 计算毛盈亏比例(考虑杠杆)
gross_pnl_ratio = leverage * price_change_ratio
# 计算开仓手续费
entry_fee = self.calculate_fee(self.entry_position_value)
# 计算平仓手续费(使用当前价格计算平仓时的仓位价值)
# 平仓时的仓位价值 ≈ 开仓时的仓位价值假设size不变
exit_position_value = self.entry_position_value
exit_fee = self.calculate_fee(exit_position_value)
# 总手续费
total_fee = entry_fee + exit_fee
# 手续费相对于保证金的比率
fee_ratio = total_fee / self.entry_margin
# 净盈亏 = 毛盈亏 - 手续费比率
net_pnl_ratio = gross_pnl_ratio - fee_ratio
return net_pnl_ratio, total_fee, gross_pnl_ratio
def close_position_all(self) -> bool:
"""
平掉所有持仓
重试机制最多尝试3次每次平仓后通过SDK验证是否成功
返回: True=平仓成功, False=平仓失败
"""
if self.pos == 0:
logger.info("当前无持仓,无需平仓")
return True
max_retries = 3
retry_delay = 1.0 # 每次重试间隔1秒
for attempt in range(1, max_retries + 1):
logger.info(f"平仓尝试 {attempt}/{max_retries}...")
# 记录平仓前的持仓状态
old_pos = self.pos
# 执行平仓操作
if self.pos == 1:
# 平多单使用side=2
ok = self.place_market_order(2, 999999)
if not ok:
logger.warning(f"平多单操作失败 (尝试 {attempt}/{max_retries})")
if attempt < max_retries:
time.sleep(retry_delay)
continue
else:
self.ding(f"平多单失败:已重试{max_retries}次仍失败", error=True)
return False
elif self.pos == -1:
# 平空单使用side=3
ok = self.place_market_order(3, 999999)
if not ok:
logger.warning(f"平空单操作失败 (尝试 {attempt}/{max_retries})")
if attempt < max_retries:
time.sleep(retry_delay)
continue
else:
self.ding(f"平空单失败:已重试{max_retries}次仍失败", error=True)
return False
else:
logger.info("持仓状态异常,无需平仓")
return True
# 等待订单执行
time.sleep(1.5) # 等待订单执行
# 通过SDK验证是否平仓成功
verify_success = self._verify_position_closed(old_pos)
if verify_success:
# 平仓成功,清空状态
self.pos = 0
self.entry_margin = None
self.entry_position_value = None
logger.success(f"✅ 平仓成功 (尝试 {attempt}/{max_retries})")
return True
else:
# 平仓失败,准备重试
logger.warning(f"平仓验证失败,持仓仍存在 (尝试 {attempt}/{max_retries})")
if attempt < max_retries:
time.sleep(retry_delay)
# 重新获取持仓状态
self.get_position_status()
else:
self.ding(f"平仓失败:已重试{max_retries}次,持仓仍未平掉", error=True)
return False
return False
def _verify_position_closed(self, expected_old_pos: int) -> bool:
"""
验证持仓是否已平仓
通过SDK查询持仓状态确认是否真的平仓成功
返回: True=平仓成功, False=仍有持仓
"""
try:
# 调用SDK查询持仓状态
resp = self.contractAPI.get_position(contract_symbol=self.cfg.contract_symbol)[0]
if resp.get("code") != 1000:
logger.warning(f"查询持仓状态失败: {resp}")
return False
positions = resp.get("data", [])
# 如果没有持仓,说明平仓成功
if not positions or len(positions) == 0:
logger.info("✅ SDK验证持仓已平仓")
return True
# 检查持仓是否还存在
# position_type: 1=多, 2=空 (根据get_position_status的逻辑)
for p in positions:
position_type = p.get("position_type", 0)
# 根据get_position_status的逻辑1=多(pos=1), 其他=空(pos=-1)
current_pos = 1 if position_type == 1 else -1
if current_pos == expected_old_pos:
# 持仓仍然存在(与平仓前的方向一致)
logger.warning(
f"⚠️ SDK验证持仓仍存在 (position_type={position_type}, 方向={'' if current_pos == 1 else ''})")
return False
# 持仓已改变或不存在(平仓成功)
logger.info("✅ SDK验证持仓已平仓或已改变")
return True
except Exception as e:
logger.error(f"验证持仓状态异常: {e}")
# 验证失败时,保守处理:认为可能未平仓
return False
# ----------------- 止损后机制 -----------------
def _reentry_penalty_active(self, dev: float, entry_dev: float) -> bool:
@@ -674,7 +880,11 @@ class BitmartFuturesMeanReversionBot:
self.pos = 1
self.entry_price = price
self.entry_ts = time.time()
self.ding(f"✅开多dev={dev * 100:.3f}% size={size} entry={price:.2f}")
# 记录开仓信息(用于手续费计算)
self.entry_margin = self.cfg.fixed_margin
self.entry_position_value = self.entry_margin * int(self.cfg.leverage)
self.ding(
f"✅开多dev={dev * 100:.3f}% size={size} entry={price:.2f} margin={self.entry_margin}u value={self.entry_position_value}u")
# ========== 【开单位置2】开空单 ==========
# 方向:空(做空,卖出)
@@ -684,56 +894,130 @@ class BitmartFuturesMeanReversionBot:
self.pos = -1
self.entry_price = price
self.entry_ts = time.time()
self.ding(f"✅开空dev={dev * 100:.3f}% size={size} entry={price:.2f}")
# 记录开仓信息(用于手续费计算)
self.entry_margin = self.cfg.fixed_margin
self.entry_position_value = self.entry_margin * int(self.cfg.leverage)
self.ding(
f"✅开空dev={dev * 100:.3f}% size={size} entry={price:.2f} margin={self.entry_margin}u value={self.entry_position_value}u")
def maybe_exit(self, price: float, tp: float, sl: float, vol_scale: float):
"""检查并执行出场"""
def maybe_exit(self, price: float, ema_value: float, tp: float, sl: float, vol_scale: float):
"""
检查并执行出场
【平仓函数】包含四种平仓条件,所有平仓都会考虑手续费:
1. 正常平仓价格回归到EMA附近时平仓扣除手续费后仍有盈利
2. 止盈:达到止盈阈值(扣除手续费后仍有盈利)
3. 止损:达到止损阈值
4. 超时平仓:持仓时间超过限制(扣除手续费后仍有盈利)
"""
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
# 计算毛盈亏和净盈亏(扣除手续费后)
# calculate_net_pnl已经考虑了杠杆倍数
net_pnl, total_fee, gross_pnl = self.calculate_net_pnl(price)
# 计算价格偏离EMA的程度
dev = (price - ema_value) / ema_value if ema_value else 0.0
# 计算止损倍数(考虑止损后放宽)
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()
# ========== 【平仓条件1】正常平仓价格回归到EMA附近 ==========
# 这是均值回归策略的核心当价格回归到EMA附近时说明均值回归已经完成应该平仓
# 但必须扣除手续费后仍有盈利才平仓
if abs(dev) <= self.cfg.normal_exit_threshold:
if net_pnl > 0: # 扣除手续费后仍有盈利
if self.close_position_all():
self.ding(
f"📊正常平仓价格回归EMA附近 dev={dev * 100:.3f}% "
f"毛盈亏={gross_pnl * 100:.3f}% 净盈亏={net_pnl * 100:.3f}% "
f"手续费={total_fee:.4f}u price={price:.2f}"
)
self.entry_price, self.entry_ts = None, None
self.entry_margin, self.entry_position_value = None, None
self.last_exit_ts = time.time()
return
else:
logger.error("正常平仓失败,持仓可能仍存在")
# 平仓失败时不更新状态,下次循环会继续尝试
return
elif pnl <= -effective_sl:
# ========== 【平仓条件2】止盈 ==========
# 达到止盈阈值,且扣除手续费后仍有盈利
if gross_pnl >= tp:
if net_pnl > 0: # 扣除手续费后仍有盈利
if self.close_position_all():
self.ding(
f"🎯止盈:毛盈亏={gross_pnl * 100:.3f}% 净盈亏={net_pnl * 100:.3f}% "
f"手续费={total_fee:.4f}u price={price:.2f} tp={tp * 100:.3f}%"
)
self.entry_price, self.entry_ts = None, None
self.entry_margin, self.entry_position_value = None, None
self.last_exit_ts = time.time()
return
else:
logger.error("止盈平仓失败,持仓可能仍存在")
return
# ========== 【平仓条件3】止损 ==========
# 止损使用毛盈亏判断(因为止损是风险控制,即使扣除手续费后亏损也要止损)
if gross_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
)
if self.close_position_all():
self.ding(
f"🛑止损:毛盈亏={gross_pnl * 100:.3f}% 净盈亏={net_pnl * 100:.3f}% "
f"手续费={total_fee:.4f}u 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.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.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()
self.entry_price, self.entry_ts = None, None
self.entry_margin, self.entry_position_value = None, None
self.last_exit_ts = time.time()
return
else:
logger.error("止损平仓失败,持仓可能仍存在,风险较高!")
self.ding("⚠️ 止损平仓失败,请手动检查持仓!", error=True)
# 即使平仓失败,也更新止损状态,避免重复触发
self.last_sl_dir = sl_dir
self.last_sl_ts = time.time()
return
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()
# ========== 【平仓条件4】超时平仓 ==========
# 超时平仓也要考虑手续费,只有扣除手续费后仍有盈利才平仓
if hold >= self.cfg.max_hold_sec:
if net_pnl > 0: # 扣除手续费后仍有盈利
if self.close_position_all():
self.ding(
f"⏱超时hold={int(hold)}s 毛盈亏={gross_pnl * 100:.3f}% "
f"净盈亏={net_pnl * 100:.3f}% 手续费={total_fee:.4f}u price={price:.2f}"
)
self.entry_price, self.entry_ts = None, None
self.entry_margin, self.entry_position_value = None, None
self.last_exit_ts = time.time()
return
else:
logger.error("超时平仓失败,持仓可能仍存在")
return
else:
# 超时但扣除手续费后亏损,继续持有等待盈利
logger.debug(
f"⏱超时但净盈亏为负继续持有hold={int(hold)}s "
f"毛盈亏={gross_pnl * 100:.3f}% 净盈亏={net_pnl * 100:.3f}%"
)
def notify_status_throttled(self, price: float, ema_value: float, dev: float, bal: float,
atr_ratio: float, base_ratio: float, vol_scale: float,
@@ -754,6 +1038,13 @@ class BitmartFuturesMeanReversionBot:
base_age = int(now - self._base_ratio_ts) if self._base_ratio_ts else -1
# 计算当前盈亏(如果有持仓)
current_gross_pnl = 0.0
current_net_pnl = 0.0
current_fee = 0.0
if self.pos != 0 and self.entry_price:
current_net_pnl, current_fee, current_gross_pnl = self.calculate_net_pnl(price)
msg = (
f"【BitMart {self.cfg.contract_symbol}1m均值回归(动态阈值)】\n"
f"📊 状态:{direction_str}\n"
@@ -762,11 +1053,23 @@ class BitmartFuturesMeanReversionBot:
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.normal_exit_threshold * 100:.3f}%\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"
f"💳 可用余额:{bal:.2f} USDT | 杠杆:{self.cfg.leverage}x | 固定保证金:{self.cfg.fixed_margin}u\n"
)
if self.pos != 0 and self.entry_price is not None and self.entry_position_value is not None:
msg += (
f"📈 当前盈亏:毛盈亏={current_gross_pnl * 100:.3f}% | "
f"净盈亏={current_net_pnl * 100:.3f}% | "
f"手续费={current_fee:.4f}u\n"
f"📍 入场价:{self.entry_price:.2f} | "
f"仓位价值:{self.entry_position_value:.2f}u\n"
)
msg += f"⏱️ 持仓限制:{self.cfg.max_hold_sec}s | 冷却:{self.cfg.cooldown_sec_after_exit}s"
self.ding(msg)
def openBrowser(self):
@@ -914,7 +1217,11 @@ class BitmartFuturesMeanReversionBot:
# 7. 检查交易是否启用
if not self.trading_enabled:
if self.pos != 0:
self.close_position_all()
if self.close_position_all():
logger.info("交易被禁用,已平仓")
else:
logger.error("交易被禁用,但平仓失败,请手动检查!")
self.ding("⚠️ 交易被禁用但平仓失败,请手动检查持仓!", error=True)
logger.warning("交易被禁用(风控触发),等待...")
time.sleep(5)
continue
@@ -922,12 +1229,14 @@ class BitmartFuturesMeanReversionBot:
# 8. 检查危险市场
if self.is_danger_market(klines, price):
logger.warning("危险模式:高波动/大实体K暂停开仓")
self.maybe_exit(price, tp, sl, vol_scale)
self.maybe_exit(price, ema_value, tp, sl, vol_scale)
time.sleep(self.cfg.tick_refresh_sec)
continue
# 9. 执行交易逻辑
self.maybe_exit(price, tp, sl, vol_scale)
# 先检查平仓(包括正常平仓、止盈、止损)
self.maybe_exit(price, ema_value, tp, sl, vol_scale)
# 再检查开仓
self.maybe_enter(price, ema_value, entry_dev)
# 10. 状态通知
@@ -971,3 +1280,4 @@ if __name__ == "__main__":
logger.error(f"程序异常退出: {e}")
bot.ding(f"❌ 策略异常退出: {e}", error=True)
raise

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.