2025-12-19 19:16:41 +08:00
|
|
|
|
import time
|
|
|
|
|
|
import uuid
|
|
|
|
|
|
import datetime
|
2025-12-08 16:20:22 +08:00
|
|
|
|
|
2025-12-19 19:16:41 +08:00
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class BitmartMarketMaker:
|
|
|
|
|
|
def __init__(self):
|
|
|
|
|
|
self.api_key = "a0fb7b98464fd9bcce67e7c519d58ec10d0c38a8"
|
|
|
|
|
|
self.secret_key = "4eaeba78e77aeaab1c2027f846a276d164f264a44c2c1bb1c5f3be50c8de1ca5"
|
|
|
|
|
|
self.memo = "合约交易"
|
|
|
|
|
|
|
|
|
|
|
|
self.contract_symbol = "ETHUSDT"
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
|
self.pbar = tqdm(total=10, desc="等待下次检查", ncols=80)
|
|
|
|
|
|
|
|
|
|
|
|
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.take_profit_pct = 2.0 # 止盈 2%
|
|
|
|
|
|
self.stop_loss_pct = 1.0 # 止损 1%(可根据风险偏好调整)
|
|
|
|
|
|
|
|
|
|
|
|
self.price_precision = 0.1
|
|
|
|
|
|
self.leverage_set = False
|
|
|
|
|
|
self.max_retries = 5
|
|
|
|
|
|
|
|
|
|
|
|
def ding(self, msg, error=False):
|
|
|
|
|
|
prefix = "❌bitmart MM:" if error else "🔔bitmart MM:"
|
|
|
|
|
|
if error:
|
|
|
|
|
|
for i in range(10):
|
|
|
|
|
|
send_dingtalk_message(f"{prefix},{msg}")
|
|
|
|
|
|
else:
|
|
|
|
|
|
send_dingtalk_message(f"{prefix},{msg}")
|
|
|
|
|
|
|
|
|
|
|
|
def try_set_leverage(self):
|
|
|
|
|
|
if self.leverage_set:
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
|
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 get_available_balance(self):
|
|
|
|
|
|
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))
|
|
|
|
|
|
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: 平空
|
|
|
|
|
|
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}")
|
|
|
|
|
|
return False
|
|
|
|
|
|
except APIException as e:
|
|
|
|
|
|
logger.error(f"API平仓异常: {e}")
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
def action(self):
|
|
|
|
|
|
logger.info("程序启动,将在循环中动态尝试设置杠杆")
|
|
|
|
|
|
|
|
|
|
|
|
while True:
|
|
|
|
|
|
if not self.try_set_leverage():
|
|
|
|
|
|
logger.warning("杠杆未设置成功,继续重试...")
|
|
|
|
|
|
|
|
|
|
|
|
if not self.get_position_status():
|
|
|
|
|
|
self.ding(error=True, msg="获取仓位信息失败!!!")
|
|
|
|
|
|
time.sleep(10)
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
# 获取当前价格(使用中价)
|
|
|
|
|
|
best_bid, best_ask = self.get_depth()
|
|
|
|
|
|
if best_bid is None or best_ask is None:
|
|
|
|
|
|
time.sleep(10)
|
|
|
|
|
|
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)
|
|
|
|
|
|
# 平仓后继续下一轮(重新挂单)
|
|
|
|
|
|
|
|
|
|
|
|
# 检查库存阈值
|
|
|
|
|
|
if abs(self.current_amount) > self.max_position_threshold:
|
|
|
|
|
|
self.close_position(reason="库存阈值")
|
|
|
|
|
|
|
|
|
|
|
|
# 挂单
|
|
|
|
|
|
bid_price = mid_price - self.spread_offset
|
|
|
|
|
|
ask_price = mid_price + self.spread_offset
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
|
|
BitmartMarketMaker().action()
|