bitmart优化完成

This commit is contained in:
Administrator
2025-12-22 11:07:50 +08:00
parent b04a1da4c3
commit 4396e49e42
8 changed files with 559 additions and 253 deletions

3
1111
View File

@@ -10,7 +10,8 @@ websea
账号yangyicheng6666@gmail.com
密码Wg138333.
yx20250715@gmail.com
Abc12345678
3. 更多交易信号
包住形态(原策略)

21
bitmart_strategy.log Normal file
View File

@@ -0,0 +1,21 @@
2025-12-20 02:51:09.247 | WARNING | __main__:__init__:28 - 请确认Memo是否正确默认Memo可能导致签名失败
2025-12-20 02:51:09.252 | INFO | __main__:init_api_client:57 - 尝试初始化API客户端 (尝试 1/3)
2025-12-20 02:51:09.253 | ERROR | __main__:test_api_connection:112 - API连接测试失败: APIContract.get_depth() got an unexpected keyword argument 'limit'
2025-12-20 02:51:09.254 | SUCCESS | __main__:init_api_client:87 - API客户端初始化成功
2025-12-20 02:51:09.254 | INFO | __main__:action:220 - 启动BitMart合约策略...
2025-12-20 02:51:09.254 | INFO | __main__:set_leverage_with_retry:121 - 尝试设置杠杆 (尝试 1/3)
2025-12-20 02:51:09.814 | ERROR | __main__:set_leverage_with_retry:149 - API异常: APIException(http status=401): response={"code":30005,"msg":"Header X-BM-SIGN is wrong","trace":"d9c107e3-7564-474e-9eaf-59667f869ae5"}
2025-12-20 02:51:09.814 | INFO | __main__:set_leverage_with_retry:161 - 等待1秒后重试...
2025-12-20 02:51:10.815 | INFO | __main__:set_leverage_with_retry:121 - 尝试设置杠杆 (尝试 2/3)
2025-12-20 02:51:10.923 | ERROR | __main__:set_leverage_with_retry:149 - API异常: APIException(http status=401): response={"trace":"a7fa76a2-2e34-435b-acbe-90e37b47fe48","code":30005,"msg":"Header X-BM-SIGN is wrong"}
2025-12-20 02:51:10.923 | INFO | __main__:set_leverage_with_retry:161 - 等待2秒后重试...
2025-12-20 02:51:12.923 | INFO | __main__:set_leverage_with_retry:121 - 尝试设置杠杆 (尝试 3/3)
2025-12-20 02:51:13.029 | ERROR | __main__:set_leverage_with_retry:149 - API异常: APIException(http status=401): response={"code":30005,"msg":"Header X-BM-SIGN is wrong","trace":"97b6c791-5812-4285-a659-dbba3f6170e6"}
2025-12-20 02:51:13.029 | WARNING | __main__:set_leverage_with_retry:168 - 杠杆设置失败,但程序将继续运行
2025-12-20 02:51:13.030 | WARNING | __main__:action:226 - 杠杆设置失败,使用系统默认杠杆继续运行
2025-12-20 02:51:13.150 | SUCCESS | __main__:action:254 - 获取到新K线: 2025-12-20 02:30:00
2025-12-20 02:51:13.150 | INFO | __main__:action:259 - 当前价格: 2959.49
2025-12-20 02:51:13.311 | INFO | __main__:action:268 - 用户中断程序

46
bitmart_trading.log Normal file
View File

@@ -0,0 +1,46 @@
2025-12-20 03:08:42.274 | INFO | __main__:initialize_api:66 - 尝试API配置 1/3
2025-12-20 03:08:42.275 | INFO | __main__:initialize_api:67 - Memo: ''
2025-12-20 03:08:42.275 | ERROR | __main__:test_api_connection:108 - API连接测试失败: 'APIContract' object has no attribute 'get_contracts'
2025-12-20 03:08:42.276 | WARNING | __main__:initialize_api:84 - API配置 1 连接失败,尝试下一个配置
2025-12-20 03:08:42.276 | INFO | __main__:initialize_api:66 - 尝试API配置 2/3
2025-12-20 03:08:42.276 | INFO | __main__:initialize_api:67 - Memo: '合约交易'
2025-12-20 03:08:42.276 | ERROR | __main__:test_api_connection:108 - API连接测试失败: 'APIContract' object has no attribute 'get_contracts'
2025-12-20 03:08:42.276 | WARNING | __main__:initialize_api:84 - API配置 2 连接失败,尝试下一个配置
2025-12-20 03:08:42.276 | INFO | __main__:initialize_api:66 - 尝试API配置 3/3
2025-12-20 03:08:42.277 | INFO | __main__:initialize_api:67 - Memo: 'None'
2025-12-20 03:08:42.277 | ERROR | __main__:test_api_connection:108 - API连接测试失败: 'APIContract' object has no attribute 'get_contracts'
2025-12-20 03:08:42.277 | WARNING | __main__:initialize_api:84 - API配置 3 连接失败,尝试下一个配置
2025-12-20 03:08:42.277 | ERROR | __main__:initialize_api:89 - 所有API配置都失败
2025-12-20 03:08:42.281 | ERROR | __main__:action:408 - API初始化失败程序无法运行
2025-12-20 03:08:48.311 | INFO | __main__:initialize_api:66 - 尝试API配置 1/3
2025-12-20 03:08:48.312 | INFO | __main__:initialize_api:67 - Memo: ''
2025-12-20 03:08:48.313 | ERROR | __main__:test_api_connection:108 - API连接测试失败: 'APIContract' object has no attribute 'get_contracts'
2025-12-20 03:08:48.313 | WARNING | __main__:initialize_api:84 - API配置 1 连接失败,尝试下一个配置
2025-12-20 03:08:48.313 | INFO | __main__:initialize_api:66 - 尝试API配置 2/3
2025-12-20 03:08:48.313 | INFO | __main__:initialize_api:67 - Memo: '合约交易'
2025-12-20 03:08:48.313 | ERROR | __main__:test_api_connection:108 - API连接测试失败: 'APIContract' object has no attribute 'get_contracts'
2025-12-20 03:08:48.313 | WARNING | __main__:initialize_api:84 - API配置 2 连接失败,尝试下一个配置
2025-12-20 03:08:48.313 | INFO | __main__:initialize_api:66 - 尝试API配置 3/3
2025-12-20 03:08:48.313 | INFO | __main__:initialize_api:67 - Memo: 'None'
2025-12-20 03:08:48.313 | ERROR | __main__:test_api_connection:108 - API连接测试失败: 'APIContract' object has no attribute 'get_contracts'
2025-12-20 03:08:48.313 | WARNING | __main__:initialize_api:84 - API配置 3 连接失败,尝试下一个配置
2025-12-20 03:08:48.314 | ERROR | __main__:initialize_api:89 - 所有API配置都失败
2025-12-20 03:08:48.317 | ERROR | __main__:action:408 - API初始化失败程序无法运行
2025-12-20 03:09:25.953 | INFO | __main__:initialize_api:66 - 尝试API配置 1/3
2025-12-20 03:09:25.954 | INFO | __main__:initialize_api:67 - Memo: ''
2025-12-20 03:09:25.954 | ERROR | __main__:test_api_connection:108 - API连接测试失败: 'APIContract' object has no attribute 'get_contracts'
2025-12-20 03:09:25.954 | WARNING | __main__:initialize_api:84 - API配置 1 连接失败,尝试下一个配置
2025-12-20 03:09:25.954 | INFO | __main__:initialize_api:66 - 尝试API配置 2/3
2025-12-20 03:09:25.954 | INFO | __main__:initialize_api:67 - Memo: '合约交易'
2025-12-20 03:09:25.954 | ERROR | __main__:test_api_connection:108 - API连接测试失败: 'APIContract' object has no attribute 'get_contracts'
2025-12-20 03:09:25.954 | WARNING | __main__:initialize_api:84 - API配置 2 连接失败,尝试下一个配置
2025-12-20 03:09:25.954 | INFO | __main__:initialize_api:66 - 尝试API配置 3/3
2025-12-20 03:09:25.954 | INFO | __main__:initialize_api:67 - Memo: 'None'
2025-12-20 03:09:25.955 | ERROR | __main__:test_api_connection:108 - API连接测试失败: 'APIContract' object has no attribute 'get_contracts'
2025-12-20 03:09:25.955 | WARNING | __main__:initialize_api:84 - API配置 3 连接失败,尝试下一个配置
2025-12-20 03:09:25.955 | ERROR | __main__:initialize_api:89 - 所有API配置都失败
2025-12-20 03:09:25.958 | ERROR | __main__:action:408 - API初始化失败程序无法运行
2025-12-20 03:12:23.884 | INFO | __main__:action:396 - 启动BitMart合约交易策略...
2025-12-20 03:12:23.885 | INFO | __main__:test_api_connection:48 - 测试API连接...
2025-12-20 03:12:24.359 | ERROR | __main__:test_api_connection:90 - API连接测试异常: 'tuple' object has no attribute 'get'
2025-12-20 03:12:24.359 | ERROR | __main__:action:400 - API连接测试失败

Binary file not shown.

Binary file not shown.

Binary file not shown.

712
test.py
View File

@@ -1,6 +1,8 @@
import os
import time
import uuid
import datetime
from dataclasses import dataclass
from tqdm import tqdm
from loguru import logger
@@ -11,290 +13,518 @@ from bitmart.lib.cloud_exceptions import APIException
from 交易.tools import send_dingtalk_message
class BitmartMarketMaker:
def __init__(self):
@dataclass
class StrategyConfig:
# ===== 合约 =====
contract_symbol: str = "ETHUSDT"
open_type: str = "cross"
leverage: str = "2" # 1~2 更稳
# ===== K线与指标 =====
step_min: int = 1
lookback_min: int = 240
ema_len: int = 30
atr_len: int = 14
# ===== 动态阈值(关键:自适应行情)=====
# entry_dev / tp / sl 都由 ATR/Price 动态计算: max(下限, 系数 * atr_ratio)
entry_dev_floor: float = 0.0010 # 0.10% 最小偏离阈值(保证平静行情也能出单)
tp_floor: float = 0.0005 # 0.05% 最小止盈
sl_floor: float = 0.0015 # 0.15% 最小止损
entry_k: float = 1.20 # entry_dev = max(floor, entry_k * atr_ratio)
tp_k: float = 0.60 # tp = max(floor, tp_k * atr_ratio)
sl_k: float = 1.20 # sl = max(floor, sl_k * atr_ratio)
max_hold_sec: int = 120 # 2分钟超时退出
cooldown_sec_after_exit: int = 10 # 平仓后冷却10秒防抖
# ===== 下单/仓位 =====
risk_percent: float = 0.0015 # 每次用可用余额的0.15%作为保证金预算
min_size: int = 1
max_size: int = 5000
# ===== 日内风控 =====
daily_loss_limit: float = 0.02 # -2% 停机
daily_profit_cap: float = 0.01 # +1% 封顶停机
# ===== 危险模式过滤(避免趋势/插针)=====
atr_ratio_kill: float = 0.0045 # ATR/Price > 0.45% -> 危险模式,暂停开仓
big_body_kill: float = 0.012 # 1m实体>1.2% -> 危险模式
# ===== 轮询节奏减少REST压力=====
klines_refresh_sec: int = 10
tick_refresh_sec: int = 1
status_notify_sec: int = 60
class BitmartFuturesMeanReversionBot:
def __init__(self, cfg: StrategyConfig):
self.cfg = cfg
# ✅ 强制只从环境变量读
self.api_key = "a0fb7b98464fd9bcce67e7c519d58ec10d0c38a8"
self.secret_key = "4eaeba78e77aeaab1c2027f846a276d164f264a44c2c1bb1c5f3be50c8de1ca5"
self.memo = "合约交易"
self.contract_symbol = "ETHUSDT"
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))
self.start = 0 # 持仓状态: -1 空, 0 无, 1 多
self.open_avg_price = None # 开仓均价字符串或float
self.current_amount = 0
self.position_cross = None
# 持仓状态: -1 空, 0 无, 1 多
self.pos = 0
self.entry_price = None
self.entry_ts = None
self.last_exit_ts = 0
self.pbar = tqdm(total=10, desc="等待下次检查", ncols=80)
# 日内权益基准
self.day_start_equity = None
self.trading_enabled = True
self.day_tag = datetime.date.today()
self.leverage = "10" # 低杠杆,安全做市
self.open_type = "cross"
self.fixed_size = 1 # 每次挂单量
self.spread_offset = 5.0 # 挂单偏移USDT建议5~10避免立即成交
self.max_position_threshold = 10 # 库存阈值,超过自动平仓
# 缓存
self._klines_cache = None
self._klines_cache_ts = 0
# 新增:止盈止损参数(基于开仓均价的百分比)
self.take_profit_pct = 2.0 # 止盈 2%
self.stop_loss_pct = 1.0 # 止损 1%(可根据风险偏好调整)
self._last_status_notify_ts = 0
self.price_precision = 0.1
self.leverage_set = False
self.max_retries = 5
self.pbar = tqdm(total=60, desc="运行中(秒)", ncols=90)
# ----------------- 通用工具 -----------------
def ding(self, msg, error=False):
prefix = "❌bitmart MM" if error else "🔔bitmart MM"
prefix = "❌bitmart" if error else "🔔bitmart"
if error:
for i in range(10):
send_dingtalk_message(f"{prefix}{msg}")
for _ in range(3):
send_dingtalk_message(f"{prefix}{msg}")
else:
send_dingtalk_message(f"{prefix}{msg}")
send_dingtalk_message(f"{prefix}{msg}")
def try_set_leverage(self):
if self.leverage_set:
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:
"""
优先取更实时的最新价若SDK不支持/字段不同回退到K线close。
"""
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
for attempt in range(self.max_retries):
self.cancel_all_orders()
time.sleep(2)
try:
response = self.contractAPI.post_submit_leverage(
contract_symbol=self.contract_symbol,
leverage=self.leverage,
open_type=self.open_type
)[0]
if response['code'] == 1000:
logger.success(f"全仓模式 + {self.leverage}x 杠杆设置成功")
self.leverage_set = True
return True
else:
logger.error(f"杠杆设置失败 (尝试 {attempt+1}): {response}")
except Exception as e:
logger.error(f"设置杠杆异常 (尝试 {attempt+1}): {e}")
time.sleep(10)
self.ding(error=True, msg="杠杆设置多次失败,请手动检查")
return False
def get_depth(self):
try:
response = self.contractAPI.get_depth(contract_symbol=self.contract_symbol)[0]
if response['code'] == 1000:
data = response['data']
best_bid = float(data['bids'][0][0]) if data['bids'] else None
best_ask = float(data['asks'][0][0]) if data['asks'] else None
return best_bid, best_ask
else:
logger.error(f"获取深度失败: {response}")
return None, None
except Exception as e:
logger.error(f"获取深度异常: {e}")
return None, None
def cancel_all_orders(self):
try:
response = self.contractAPI.post_cancel_orders(contract_symbol=self.contract_symbol)[0]
if response['code'] == 1000:
logger.success("所有挂单已取消")
return True
else:
logger.error(f"取消挂单失败: {response}")
return False
except Exception as e:
logger.warning(f"取消挂单异常(忽略): {e}")
return False
def place_limit_order(self, side: int, price: float, size: int):
price = round(price / self.price_precision) * self.price_precision
price_str = f"{price:.1f}"
client_order_id = f"mm_{int(time.time())}_{uuid.uuid4().hex[:8]}"
for attempt in range(self.max_retries):
try:
response = self.contractAPI.post_submit_order(
contract_symbol=self.contract_symbol,
client_order_id=client_order_id,
side=side,
mode=1,
type='limit',
leverage=self.leverage,
open_type=self.open_type,
price=price_str,
size=size
)[0]
if response['code'] == 1000:
action = "挂买(开多)" if side == 1 else "挂卖(开空)"
logger.success(f"限价单挂单成功: {action}, 价格={price}, 张数={size}")
return True
else:
logger.error(f"挂单失败 (尝试 {attempt+1}): {response}")
except APIException as e:
logger.error(f"API挂单异常 (尝试 {attempt+1}): {e}")
time.sleep(5)
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 get_position_status(self):
try:
response = self.contractAPI.get_position(contract_symbol=self.contract_symbol)[0]
if response['code'] == 1000:
positions = response['data']
if not positions:
self.start = 0
self.current_amount = 0
self.open_avg_price = None
return True
position = positions[0]
self.start = 1 if position['position_type'] == 1 else -1
self.open_avg_price = float(position['open_avg_price']) if position['open_avg_price'] else 0.0
self.current_amount = int(float(position.get('current_amount', 0)))
self.position_cross = position.get("position_cross")
return True
else:
return False
except Exception as e:
logger.error(f"持仓查询异常: {e}")
self.ding(error=True, msg="持仓查询异常")
return False
def dynamic_thresholds(self, atr_ratio: float):
"""
返回动态 entry_dev / tp / sl
"""
entry_dev = max(self.cfg.entry_dev_floor, self.cfg.entry_k * atr_ratio)
tp = max(self.cfg.tp_floor, self.cfg.tp_k * atr_ratio)
sl = max(self.cfg.sl_floor, self.cfg.sl_k * atr_ratio)
return entry_dev, tp, sl
def get_available_balance(self):
# ----------------- 账户/仓位 -----------------
def get_assets_available(self) -> float:
try:
response = self.contractAPI.get_assets_detail()[0]
if response['code'] == 1000:
data = response['data']
if isinstance(data, dict):
return float(data.get('available_balance', 0))
elif isinstance(data, list):
for asset in data:
if asset.get('currency') == 'USDT':
return float(asset.get('available_balance', 0))
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 check_take_profit_stop_loss(self, current_price: float):
"""检查是否触发止盈或止损(基于当前价格)"""
if self.current_amount == 0 or self.open_avg_price == 0.0:
return False, None
if self.start == 1: # 多头
pnl_pct = (current_price - self.open_avg_price) / self.open_avg_price * 100
if pnl_pct >= self.take_profit_pct:
return True, "止盈平多"
if pnl_pct <= -self.stop_loss_pct:
return True, "止损平多"
elif self.start == -1: # 空头
pnl_pct = (self.open_avg_price - current_price) / self.open_avg_price * 100
if pnl_pct >= self.take_profit_pct:
return True, "止盈平空"
if pnl_pct <= -self.stop_loss_pct:
return True, "止损平空"
return False, None
def close_position(self, reason: str = "库存阈值"):
"""市场全平仓,并发送通知"""
if self.current_amount == 0:
return
side = 3 if self.start == 1 else 2 # 3: 平多, 2: 平空
def get_position_status(self) -> bool:
try:
response = self.contractAPI.post_submit_order(
contract_symbol=self.contract_symbol,
client_order_id=f"close_{reason}_{int(time.time())}",
side=side,
mode=1,
type='market',
leverage=self.leverage,
open_type=self.open_type,
size=999999
)[0]
if response['code'] == 1000:
direction_str = "" if self.start == 1 else ""
logger.success(f"{reason}平仓成功: 平{direction_str}")
self.ding(msg=f"{reason}触发,已平仓 {direction_str}头仓位")
self.start = 0
self.current_amount = 0
self.open_avg_price = None
return True
else:
logger.error(f"平仓失败: {response}")
resp = self.contractAPI.get_position(contract_symbol=self.cfg.contract_symbol)[0]
if resp.get("code") != 1000:
return False
except APIException as e:
logger.error(f"API平仓异常: {e}")
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:
"""
保守仓位估算:按 1张≈0.001ETH(你原假设)
"""
bal = self.get_assets_available()
if bal < 10:
return 0
margin = bal * self.cfg.risk_percent
lev = int(self.cfg.leverage)
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:
"""
side:
1 开多
2 平空
3 平多
4 开空
"""
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 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)
logger.info(
f"enter_check: price={price:.2f}, ema={ema_value:.2f}, dev={dev*100:.3f}% "
f"(阈值={entry_dev*100:.3f}%), size={size}, pos={self.pos}"
)
if size <= 0:
return
if dev <= -entry_dev:
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 >= entry_dev:
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):
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
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 <= -sl:
self.close_position_all()
self.ding(f"🛑止损pnl={pnl*100:.3f}% price={price:.2f} sl={sl*100:.3f}%", error=True)
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, entry_dev: float, tp: float, sl: 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 "")
msg = (
f"【BitMart {self.cfg.contract_symbol}|均价回归微利(动态阈值)】\n"
f"方向:{direction_str}\n"
f"现价:{price:.2f}\n"
f"EMA{self.cfg.ema_len}{ema_value:.2f}\n"
f"dev{dev*100:.3f}%(阈值{entry_dev*100:.3f}%\n"
f"ATR比{atr_ratio*100:.3f}%\n"
f"tp/sl{tp*100:.3f}% / {sl*100:.3f}%\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):
logger.info("程序启动,将在循环中动态尝试设置杠杆")
if not self.set_leverage():
self.ding("杠杆设置失败,停止运行", error=True)
return
while True:
if not self.try_set_leverage():
logger.warning("杠杆未设置成功,继续重试...")
now_dt = datetime.datetime.now()
self.pbar.n = now_dt.second
self.pbar.refresh()
klines = self.get_klines_cached()
if not klines or len(klines) < (self.cfg.ema_len + 5):
time.sleep(1)
continue
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)
# 用更实时的价格触发取不到就回退K线close
price = self.get_last_price(fallback_close=float(last_k["close"]))
dev = (price - ema_value) / ema_value if ema_value else 0.0
# 动态阈值:跟随 ATR/Price
a = self.atr(klines, self.cfg.atr_len)
atr_ratio = (a / price) if price > 0 else 0.0
entry_dev, tp, sl = self.dynamic_thresholds(atr_ratio)
# 日内风控
self.risk_kill_switch()
# 刷新仓位
if not self.get_position_status():
self.ding(error=True, msg="获取仓位信息失败!!!")
time.sleep(10)
time.sleep(1)
continue
# 获取当前价格(使用中价)
best_bid, best_ask = self.get_depth()
if best_bid is None or best_ask is None:
time.sleep(10)
# 停机:平仓+不再开仓
if not self.trading_enabled:
if self.pos != 0:
self.close_position_all()
time.sleep(5)
continue
mid_price = (best_bid + best_ask) / 2
# 检查止盈止损
trigger, reason = self.check_take_profit_stop_loss(mid_price)
if trigger:
self.close_position(reason=reason)
# 平仓后继续下一轮(重新挂单)
# 危险市场不新开仓允许已有仓按tp/sl/超时退出)
if self.is_danger_market(klines, price):
logger.warning("危险模式:高波动/大实体K暂停开仓")
self.maybe_exit(price, tp, sl)
time.sleep(self.cfg.tick_refresh_sec)
continue
# 检查库存阈值
if abs(self.current_amount) > self.max_position_threshold:
self.close_position(reason="库存阈值")
# 先出场再入场
self.maybe_exit(price, tp, sl)
self.maybe_enter(price, ema_value, entry_dev)
# 挂单
bid_price = mid_price - self.spread_offset
ask_price = mid_price + self.spread_offset
# 状态通知(限频)
bal = self.get_assets_available()
self.notify_status_throttled(price, ema_value, dev, bal, atr_ratio, entry_dev, tp, sl)
self.cancel_all_orders()
success_bid = self.place_limit_order(side=1, price=bid_price, size=self.fixed_size)
success_ask = self.place_limit_order(side=4, price=ask_price, size=self.fixed_size)
# 统计盈亏百分比
pnl_pct_str = ""
if self.current_amount != 0 and self.open_avg_price:
if self.start == 1:
pnl_pct = (mid_price - self.open_avg_price) / self.open_avg_price * 100
else:
pnl_pct = (self.open_avg_price - mid_price) / self.open_avg_price * 100
pnl_pct_str = f"浮动盈亏:{pnl_pct:+.2f}%"
balance = self.get_available_balance()
leverage_status = "已设置" if self.leverage_set else "未同步(重试中)"
msg = (
f"【BitMart {self.contract_symbol} MM】\n"
f"杠杆状态:{leverage_status}\n"
f"当前中价:{mid_price:.2f} USDT\n"
f"挂买价:{bid_price:.2f} ({'成功' if success_bid else '失败'})\n"
f"挂卖价:{ask_price:.2f} ({'成功' if success_ask else '失败'})\n"
f"持仓量:{self.current_amount}{pnl_pct_str}\n"
f"账户可用余额:{balance:.2f} USDT\n"
f"止盈:+{self.take_profit_pct}% | 止损:-{self.stop_loss_pct}%"
)
self.ding(msg=msg)
self.pbar.reset()
time.sleep(10)
time.sleep(self.cfg.tick_refresh_sec)
if __name__ == '__main__':
BitmartMarketMaker().action()
if __name__ == "__main__":
"""
Windows 设置环境变量示例PowerShell
setx BITMART_API_KEY "a0fb7b98464fd9bcce67e7c519d58ec10d0c38a8"
setx BITMART_SECRET_KEY "4eaeba78e77aeaab1c2027f846a276d164f264a44c2c1bb1c5f3be50c8de1ca5"
setx BITMART_MEMO "合约交易"
重新打开终端再运行。
"""
cfg = StrategyConfig()
bot = BitmartFuturesMeanReversionBot(cfg)
bot.action()

View File

@@ -558,9 +558,9 @@ class MessageSender:
legacy_direction = position_data.get('legacyOrderDirection', '')
# 确定方向
if position_side == 'SHORT' or legacy_direction == 'OPEN_SHORT':
if legacy_direction == 'OPEN_SHORT':
direction = ""
elif position_side == 'LONG' or legacy_direction == 'OPEN_LONG':
elif legacy_direction == 'OPEN_LONG':
direction = ""
else:
direction = ""
@@ -575,23 +575,31 @@ class MessageSender:
pnl_rate = (open_avg_price - current_price) / open_avg_price * 100
pnl_str = f"{unrealized_pnl:+.2f} USDT ({pnl_rate:+.2f}%)"
# 当前持仓名义价值
current_value = fill_size * current_price
return (
"**【WEEX ETHUSDT 永续持仓监控】**\n\n"
f"**持仓方向**{direction}\n"
f"**当前现价**{current_price:.2f} USDT\n"
f"**开仓均价**{open_avg_price:.2f} USDT\n"
f"**持仓数量(eth)**{fill_size:.3f} ETH\n"
f"**持仓数量(usdt)**{fill_value / 100:.2f} USDT\n"
f"**名义价值**{current_value:.2f} USDT\n"
f"**浮动盈亏**{pnl_str}\n"
f"**账户可用余额**{available_balance:.2f} USDT"
)
else:
pnl_str = "0.00 USDT"
# 当前持仓名义价值
current_value = fill_size * current_price
return (
"**【WEEX ETHUSDT 永续持仓监控】**\n\n"
f"**持仓方向**{direction}\n"
f"**持仓方向**\n"
f"**当前现价**{current_price:.2f} USDT\n"
f"**开仓均价**{open_avg_price:.2f} USDT\n"
f"**持仓数量(eth)**{fill_size:.3f} ETH\n"
f"**持仓数量(usdt)**{fill_value / 100:.2f} USDT\n"
f"**名义价值**{current_value:.2f} USDT\n"
f"**浮动盈亏**{pnl_str}\n"
f"**账户可用余额**{available_balance:.2f} USDT"
)
else:
return (
"**【WEEX ETHUSDT 永续持仓监控】**\n\n"