Files
lm_code/bitmart/优化拿仓版本.py
2025-12-24 18:34:19 +08:00

459 lines
17 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
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()