987 lines
37 KiB
Python
987 lines
37 KiB
Python
"""
|
||
BitMart 被动做市/高频刷单策略
|
||
核心逻辑:在盘口两侧不断挂单,赚取价差+返佣
|
||
使用浏览器自动化下单,获取高返佣
|
||
"""
|
||
|
||
import time
|
||
from loguru import logger
|
||
from threading import Lock
|
||
from dataclasses import dataclass
|
||
from bitmart.api_contract import APIContract
|
||
from typing import Optional, Dict, List, Tuple
|
||
from DrissionPage import ChromiumPage, ChromiumOptions
|
||
|
||
from bit_tools import openBrowser
|
||
|
||
|
||
# ================================================================
|
||
# 📊 配置类
|
||
# ================================================================
|
||
|
||
@dataclass
|
||
class MarketMakingConfig:
|
||
bit_id: str = "f2320f57e24c45529a009e1541e25961"
|
||
"""做市策略配置"""
|
||
# 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.01 # 价差百分比(0.01% = 买一卖一之间)
|
||
order_size_usdt: float = 10.0 # 每单金额(USDT)
|
||
max_position_usdt: float = 100.0 # 最大持仓金额(USDT)
|
||
|
||
# 订单管理
|
||
order_refresh_interval: float = 2.0 # 订单刷新间隔(秒)
|
||
order_timeout: float = 60.0 # 订单超时时间(秒),超时后撤单重新挂
|
||
|
||
# 风险控制
|
||
max_daily_loss: float = 50.0 # 每日最大亏损(USDT)
|
||
max_daily_trades: int = 1000 # 每日最大交易次数
|
||
|
||
# 杠杆和模式
|
||
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 best_bid(self) -> Optional[float]:
|
||
"""买一价"""
|
||
return self.bids[0][0] if self.bids else None
|
||
|
||
@property
|
||
def best_ask(self) -> Optional[float]:
|
||
"""卖一价"""
|
||
return self.asks[0][0] if self.asks else None
|
||
|
||
@property
|
||
def spread(self) -> Optional[float]:
|
||
"""价差"""
|
||
if self.best_bid and self.best_ask:
|
||
return self.best_ask - self.best_bid
|
||
return None
|
||
|
||
@property
|
||
def mid_price(self) -> Optional[float]:
|
||
"""中间价"""
|
||
if self.best_bid and self.best_ask:
|
||
return (self.best_bid + self.best_ask) / 2
|
||
return None
|
||
|
||
|
||
@dataclass
|
||
class PendingOrder:
|
||
"""pending订单信息"""
|
||
order_id: str
|
||
side: str # "buy" or "sell"
|
||
price: float
|
||
size: float
|
||
create_time: float
|
||
status: str # "pending", "filled", "cancelled"
|
||
|
||
|
||
# ================================================================
|
||
# 📊 浏览器管理器
|
||
# ================================================================
|
||
|
||
class BrowserManager:
|
||
"""浏览器管理器:负责浏览器的启动、接管和标签页管理"""
|
||
|
||
def __init__(self, config: MarketMakingConfig, bit_id="f2320f57e24c45529a009e1541e25961"):
|
||
self.bit_id = "f2320f57e24c45529a009e1541e25961"
|
||
self.config = config
|
||
self.tge_port: Optional[int] = None
|
||
self.page: Optional[ChromiumPage] = None
|
||
|
||
def open_browser(self) -> bool:
|
||
"""打开浏览器并获取端口"""
|
||
try:
|
||
bit_port = openBrowser(id=self.bit_id)
|
||
co = ChromiumOptions()
|
||
co.set_local_port(port=bit_port)
|
||
self.page = ChromiumPage(addr_or_opts=co)
|
||
return True
|
||
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)
|
||
self.page.set.window.max()
|
||
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()
|
||
closed_count = 0
|
||
for idx, tab in enumerate(tabs):
|
||
if idx == 0:
|
||
continue
|
||
tab.close()
|
||
closed_count += 1
|
||
if closed_count > 0:
|
||
logger.info(f"已关闭{closed_count}个多余标签页")
|
||
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
|
||
"""
|
||
开单操作(通过浏览器自动化,获取高返佣)
|
||
|
||
Args:
|
||
marketPriceLongOrder: 市价最多或者做空,1是最多,-1是做空
|
||
limitPriceShortOrder: 限价最多或者做空,1是最多,-1是做空
|
||
size: 数量
|
||
price: 价格(限价单需要)
|
||
|
||
Returns:
|
||
是否成功
|
||
"""
|
||
try:
|
||
# 市价单
|
||
if marketPriceLongOrder == -1:
|
||
# 市价做空
|
||
if not self.click_safe('x://button[normalize-space(text()) ="市价"]'):
|
||
return False
|
||
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}")
|
||
return True
|
||
|
||
elif marketPriceLongOrder == 1:
|
||
# 市价做多
|
||
if not self.click_safe('x://button[normalize-space(text()) ="市价"]'):
|
||
return False
|
||
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}")
|
||
return True
|
||
|
||
# 限价单
|
||
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(1)
|
||
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(1)
|
||
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 平仓(self) -> bool:
|
||
"""市价平仓"""
|
||
try:
|
||
if self.click_safe('x://span[normalize-space(text()) ="市价"]'):
|
||
logger.success("平仓成功")
|
||
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:
|
||
"""
|
||
下限价单(通过浏览器)
|
||
|
||
Args:
|
||
side: "buy" 或 "sell"
|
||
price: 价格
|
||
size: 数量(张数)
|
||
|
||
Returns:
|
||
是否成功
|
||
"""
|
||
try:
|
||
# size已经是张数,直接使用
|
||
if side == "buy":
|
||
# 限价做多
|
||
return self.开单(limitPriceShortOrder=1, size=size, price=price)
|
||
else:
|
||
# 限价做空
|
||
return self.开单(limitPriceShortOrder=-1, size=size, price=price)
|
||
except Exception as e:
|
||
logger.error(f"限价下单异常: {e}")
|
||
return False
|
||
|
||
|
||
# ================================================================
|
||
# 📊 BitMart API 封装(仅用于查询,不下单)
|
||
# ================================================================
|
||
|
||
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, depth: int = 20) -> Optional[OrderBook]:
|
||
"""
|
||
获取订单簿
|
||
|
||
Args:
|
||
depth: 深度数量(可能不使用)
|
||
|
||
Returns:
|
||
OrderBook对象或None
|
||
"""
|
||
try:
|
||
# BitMart合约API获取深度数据
|
||
# 根据错误信息,get_depth()不接受size参数
|
||
# 尝试不同的调用方式
|
||
try:
|
||
# 方法1:不传深度参数,使用默认值(最可能的方式)
|
||
response = self.contractAPI.get_depth(
|
||
contract_symbol=self.config.contract_symbol
|
||
)[0]
|
||
except TypeError as e1:
|
||
try:
|
||
# 方法2:尝试使用 limit 参数
|
||
response = self.contractAPI.get_depth(
|
||
contract_symbol=self.config.contract_symbol,
|
||
limit=depth
|
||
)[0]
|
||
except TypeError as e2:
|
||
try:
|
||
# 方法3:尝试使用 depth 参数
|
||
response = self.contractAPI.get_depth(
|
||
contract_symbol=self.config.contract_symbol,
|
||
depth=depth
|
||
)[0]
|
||
except TypeError as e3:
|
||
logger.error(f"get_depth()方法调用失败,尝试的参数方式都失败: {e1}, {e2}, {e3}")
|
||
return None
|
||
|
||
if response.get('code') == 1000:
|
||
data = response.get('data', {})
|
||
# BitMart返回格式可能是不同的,需要根据实际调整
|
||
bids = []
|
||
asks = []
|
||
|
||
if isinstance(data, dict):
|
||
bids_raw = data.get('bids', [])
|
||
asks_raw = data.get('asks', [])
|
||
|
||
# 处理不同格式
|
||
for b in bids_raw:
|
||
if isinstance(b, (list, tuple)) and len(b) >= 2:
|
||
bids.append((float(b[0]), float(b[1])))
|
||
elif isinstance(b, dict):
|
||
bids.append((float(b.get('price', 0)), float(b.get('size', 0))))
|
||
|
||
for a in asks_raw:
|
||
if isinstance(a, (list, tuple)) and len(a) >= 2:
|
||
asks.append((float(a[0]), float(a[1])))
|
||
elif isinstance(a, dict):
|
||
asks.append((float(a.get('price', 0)), float(a.get('size', 0))))
|
||
|
||
# 买盘按价格从高到低排序,卖盘按价格从低到高排序
|
||
bids.sort(key=lambda x: x[0], reverse=True)
|
||
asks.sort(key=lambda x: x[0])
|
||
|
||
if bids and asks:
|
||
return OrderBook(
|
||
bids=bids,
|
||
asks=asks,
|
||
timestamp=time.time()
|
||
)
|
||
return None
|
||
except Exception as e:
|
||
logger.error(f"获取订单簿异常: {e}")
|
||
# 如果获取订单簿失败,尝试使用最新价格作为备用方案
|
||
logger.warning("尝试使用最新价格作为备用方案")
|
||
current_price = self.get_current_price()
|
||
if current_price:
|
||
# 使用当前价格和价差百分比计算买一卖一
|
||
spread_amount = current_price * self.config.spread_percent / 100
|
||
bids = [(current_price - spread_amount / 2, 1.0)]
|
||
asks = [(current_price + spread_amount / 2, 1.0)]
|
||
return OrderBook(
|
||
bids=bids,
|
||
asks=asks,
|
||
timestamp=time.time()
|
||
)
|
||
return None
|
||
|
||
def get_current_price(self) -> Optional[float]:
|
||
"""获取当前最新价格"""
|
||
try:
|
||
end_time = int(time.time())
|
||
response = self.contractAPI.get_kline(
|
||
contract_symbol=self.config.contract_symbol,
|
||
step=1, # 1分钟
|
||
start_time=end_time - 60,
|
||
end_time=end_time
|
||
)[0]
|
||
|
||
if response.get('code') == 1000:
|
||
data = response.get('data', [])
|
||
if data:
|
||
return float(data[-1]["close_price"])
|
||
return None
|
||
except Exception as e:
|
||
logger.error(f"获取价格异常: {e}")
|
||
return None
|
||
|
||
def get_available_balance(self) -> Optional[float]:
|
||
"""获取合约账户可用USDT余额"""
|
||
try:
|
||
response = self.contractAPI.get_assets_detail()[0]
|
||
if response.get('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 None
|
||
except Exception as e:
|
||
logger.error(f"余额查询异常: {e}")
|
||
return None
|
||
|
||
def get_position(self) -> Optional[Dict]:
|
||
"""获取当前持仓"""
|
||
try:
|
||
response = self.contractAPI.get_position(
|
||
contract_symbol=self.config.contract_symbol
|
||
)[0]
|
||
|
||
if response.get('code') == 1000:
|
||
positions = response.get('data', [])
|
||
if positions:
|
||
return positions[0]
|
||
return None
|
||
return None
|
||
except Exception as e:
|
||
logger.error(f"持仓查询异常: {e}")
|
||
return None
|
||
|
||
def set_leverage(self) -> bool:
|
||
"""设置杠杆和全仓模式"""
|
||
try:
|
||
response = self.contractAPI.post_submit_leverage(
|
||
contract_symbol=self.config.contract_symbol,
|
||
leverage=self.config.leverage,
|
||
open_type=self.config.open_type
|
||
)[0]
|
||
|
||
if response.get('code') == 1000:
|
||
logger.success(f"全仓模式 + {self.config.leverage}x 杠杆设置成功")
|
||
return True
|
||
else:
|
||
logger.error(f"杠杆设置失败: {response}")
|
||
return False
|
||
except Exception as e:
|
||
logger.error(f"设置杠杆异常: {e}")
|
||
return False
|
||
|
||
# ============== 新增:撤单、平仓 ==============
|
||
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:
|
||
data = resp.get("data", [])
|
||
return data if isinstance(data, list) else []
|
||
return []
|
||
except Exception as e:
|
||
logger.error(f"查询挂单异常: {e}")
|
||
return []
|
||
|
||
def cancel_order(self, order_id: str) -> bool:
|
||
"""撤销单个挂单"""
|
||
try:
|
||
resp = self.contractAPI.post_cancel_order(
|
||
contract_symbol=self.config.contract_symbol,
|
||
order_id=order_id
|
||
)[0]
|
||
if resp.get("code") == 1000:
|
||
logger.success(f"撤单成功: {order_id}")
|
||
return True
|
||
logger.error(f"撤单失败: {resp}")
|
||
return False
|
||
except Exception as e:
|
||
logger.error(f"撤单异常: {e}")
|
||
return False
|
||
|
||
def cancel_all_orders(self) -> None:
|
||
"""撤销所有挂单(无精确超时信息时,直接全撤)"""
|
||
open_orders = self.get_open_orders()
|
||
for od in open_orders:
|
||
oid = str(od.get("order_id") or od.get("id") or "")
|
||
if oid:
|
||
self.cancel_order(oid)
|
||
|
||
def close_position(self) -> bool:
|
||
"""
|
||
使用API平仓(市价/近似市价)
|
||
逻辑:查询当前持仓,根据方向下相反方向的平仓单
|
||
"""
|
||
try:
|
||
position = self.get_position()
|
||
if not position:
|
||
logger.info("无持仓,无需平仓")
|
||
return True
|
||
|
||
position_type = int(position.get("position_type", 0)) # 1=多, 2=空
|
||
current_amount = float(position.get("current_amount", 0))
|
||
if current_amount <= 0:
|
||
logger.info("持仓数量为0,无需平仓")
|
||
return True
|
||
|
||
# 获取现价作为平仓价格参考
|
||
current_price = self.get_current_price()
|
||
if not current_price:
|
||
logger.error("无法获取现价,平仓失败")
|
||
return False
|
||
|
||
# BitMart合约订单类型:3=平多(限价),4=平空(限价)
|
||
if position_type == 1:
|
||
order_type = 3 # 平多
|
||
elif position_type == 2:
|
||
order_type = 4 # 平空
|
||
else:
|
||
logger.error(f"未知持仓方向: {position_type}")
|
||
return False
|
||
|
||
# 下平仓单
|
||
resp = self.contractAPI.post_submit_order(
|
||
contract_symbol=self.config.contract_symbol,
|
||
type=order_type,
|
||
price=str(current_price),
|
||
size=str(current_amount)
|
||
)[0]
|
||
|
||
if resp.get("code") == 1000:
|
||
logger.success(f"API平仓成功,方向={position_type}, 数量={current_amount}, 价格={current_price}")
|
||
return True
|
||
|
||
logger.error(f"API平仓失败: {resp}")
|
||
return False
|
||
except Exception as e:
|
||
logger.error(f"API平仓异常: {e}")
|
||
return False
|
||
|
||
|
||
# ================================================================
|
||
# 📊 做市策略核心
|
||
# ================================================================
|
||
|
||
class MarketMakingStrategy:
|
||
"""被动做市策略(使用浏览器自动化下单,获取高返佣)"""
|
||
|
||
def __init__(self, config: MarketMakingConfig, bit_id=None):
|
||
self.bit_id = bit_id
|
||
self.config = config
|
||
self.api = BitMartMarketMakerAPI(config) # 仅用于查询
|
||
|
||
# 浏览器管理
|
||
self.browser_manager = BrowserManager(config)
|
||
self.trading_executor: Optional[BrowserTradingExecutor] = None
|
||
|
||
# 订单管理(使用时间戳作为订单ID,因为浏览器下单无法直接获取订单ID)
|
||
self.pending_orders: Dict[str, PendingOrder] = {}
|
||
self.order_lock = Lock()
|
||
|
||
# 统计
|
||
self.daily_trades = 0
|
||
self.daily_profit = 0.0
|
||
self.total_trades = 0
|
||
self.total_profit = 0.0
|
||
|
||
# 运行状态
|
||
self.running = False
|
||
self.last_order_refresh = 0.0
|
||
|
||
# 初始化浏览器和杠杆
|
||
if not self._initialize_browser():
|
||
raise Exception("浏览器初始化失败")
|
||
self.api.set_leverage()
|
||
|
||
def _initialize_browser(self) -> bool:
|
||
"""初始化浏览器"""
|
||
try:
|
||
# 打开浏览器
|
||
if not self.browser_manager.open_browser():
|
||
logger.error("打开浏览器失败")
|
||
return False
|
||
|
||
# # 接管浏览器
|
||
# if not self.browser_manager.take_over_browser():
|
||
# logger.error("接管浏览器失败")
|
||
# return False
|
||
|
||
# 关闭多余标签页
|
||
self.browser_manager.close_extra_tabs()
|
||
|
||
# 打开交易页面
|
||
self.browser_manager.page.get(self.config.trading_url)
|
||
time.sleep(2) # 等待页面加载
|
||
|
||
# 初始化交易执行器
|
||
self.trading_executor = BrowserTradingExecutor(self.browser_manager.page)
|
||
|
||
logger.success("浏览器初始化完成")
|
||
return True
|
||
except Exception as e:
|
||
logger.error(f"浏览器初始化异常: {e}")
|
||
return False
|
||
|
||
def calculate_order_prices(self, order_book: OrderBook) -> Tuple[Optional[float], Optional[float]]:
|
||
"""
|
||
计算挂单价格
|
||
|
||
Args:
|
||
order_book: 订单簿
|
||
|
||
Returns:
|
||
(buy_price, sell_price)
|
||
"""
|
||
if not order_book.mid_price or not order_book.best_bid or not order_book.best_ask:
|
||
return None, None
|
||
|
||
mid = order_book.mid_price
|
||
spread_amount = mid * self.config.spread_percent / 100
|
||
|
||
# 买单价格:中间价 - 价差的一半,但不能低于买一
|
||
buy_price = mid - spread_amount / 2
|
||
buy_price = min(buy_price, order_book.best_bid * 0.9999) # 略低于买一,确保能成交
|
||
|
||
# 卖单价格:中间价 + 价差的一半,但不能高于卖一
|
||
sell_price = mid + spread_amount / 2
|
||
sell_price = max(sell_price, order_book.best_ask * 1.0001) # 略高于卖一,确保能成交
|
||
|
||
# 确保价差合理
|
||
if sell_price <= buy_price:
|
||
# 如果价差太小,使用买一卖一价格
|
||
buy_price = order_book.best_bid * 0.9999
|
||
sell_price = order_book.best_ask * 1.0001
|
||
|
||
return buy_price, sell_price
|
||
|
||
def should_refresh_orders(self) -> bool:
|
||
"""判断是否需要刷新订单"""
|
||
now = time.time()
|
||
if now - self.last_order_refresh >= self.config.order_refresh_interval:
|
||
return True
|
||
return False
|
||
|
||
def cancel_stale_orders(self):
|
||
"""撤销超时订单(使用API撤单)"""
|
||
now = time.time()
|
||
to_cancel = []
|
||
|
||
with self.order_lock:
|
||
for order_id, order in self.pending_orders.items():
|
||
if order.status == "pending":
|
||
if now - order.create_time > self.config.order_timeout:
|
||
to_cancel.append(order_id)
|
||
|
||
if not to_cancel:
|
||
return
|
||
|
||
logger.info(f"发现{len(to_cancel)}个超时订单,尝试API撤单")
|
||
try:
|
||
# 先通过API获取真实挂单列表并撤单
|
||
self.api.cancel_all_orders()
|
||
# 本地状态同步
|
||
with self.order_lock:
|
||
for order_id in to_cancel:
|
||
if order_id in self.pending_orders:
|
||
self.pending_orders[order_id].status = "cancelled"
|
||
except Exception as e:
|
||
logger.error(f"API撤单失败: {e}")
|
||
|
||
def update_pending_orders(self):
|
||
"""更新挂单状态(通过持仓变化判断订单是否成交)"""
|
||
try:
|
||
# 获取当前持仓
|
||
current_position = self.api.get_position()
|
||
current_position_type = 0
|
||
current_position_amount = 0.0
|
||
|
||
if current_position:
|
||
current_position_type = int(current_position.get('position_type', 0))
|
||
current_position_amount = abs(float(current_position.get('current_amount', 0)))
|
||
|
||
with self.order_lock:
|
||
# 检查挂单是否成交(通过持仓变化判断)
|
||
for order_id, order in list(self.pending_orders.items()):
|
||
if order.status == "pending":
|
||
# 检查订单是否超时
|
||
if time.time() - order.create_time > self.config.order_timeout:
|
||
# 订单超时,标记为取消
|
||
order.status = "cancelled"
|
||
logger.info(f"订单超时: {order_id} {order.side} @ {order.price}")
|
||
continue
|
||
except Exception as e:
|
||
logger.error(f"更新挂单状态异常: {e}")
|
||
|
||
def _place_counter_order(self, filled_order: PendingOrder):
|
||
"""
|
||
订单成交后,在另一侧挂单
|
||
|
||
Args:
|
||
filled_order: 已成交的订单
|
||
"""
|
||
# 等待一小段时间,确保订单状态更新
|
||
time.sleep(0.1)
|
||
|
||
order_book = self.api.get_order_book()
|
||
if not order_book:
|
||
logger.warning("无法获取订单簿,无法挂反向单")
|
||
return
|
||
|
||
# 计算反向订单价格
|
||
buy_price, sell_price = self.calculate_order_prices(order_book)
|
||
|
||
if filled_order.side == "buy":
|
||
# 买单成交,挂卖单
|
||
if sell_price and self.trading_executor:
|
||
contract_size = self.config.order_size_usdt / sell_price / 0.01
|
||
if contract_size < 1:
|
||
contract_size = 1
|
||
|
||
if self.trading_executor.place_limit_order("sell", sell_price, contract_size):
|
||
order_id = f"sell_{int(time.time() * 1000)}"
|
||
with self.order_lock:
|
||
self.pending_orders[order_id] = PendingOrder(
|
||
order_id=order_id,
|
||
side="sell",
|
||
price=sell_price,
|
||
size=self.config.order_size_usdt,
|
||
create_time=time.time(),
|
||
status="pending"
|
||
)
|
||
logger.info(f"买单成交后挂卖单: {sell_price}, 订单ID: {order_id}")
|
||
else:
|
||
logger.warning("买单成交后挂卖单失败")
|
||
else:
|
||
# 卖单成交,挂买单平空或开多
|
||
if buy_price and self.trading_executor:
|
||
contract_size = self.config.order_size_usdt / buy_price / 0.01
|
||
if contract_size < 1:
|
||
contract_size = 1
|
||
|
||
if self.trading_executor.place_limit_order("buy", buy_price, contract_size):
|
||
order_id = f"buy_{int(time.time() * 1000)}"
|
||
with self.order_lock:
|
||
self.pending_orders[order_id] = PendingOrder(
|
||
order_id=order_id,
|
||
side="buy",
|
||
price=buy_price,
|
||
size=self.config.order_size_usdt,
|
||
create_time=time.time(),
|
||
status="pending"
|
||
)
|
||
logger.info(f"卖单成交后挂买单: {buy_price}, 订单ID: {order_id}")
|
||
else:
|
||
logger.warning("卖单成交后挂买单失败")
|
||
|
||
def place_market_making_orders(self):
|
||
"""放置做市订单"""
|
||
# 获取订单簿
|
||
order_book = self.api.get_order_book()
|
||
if not order_book or not order_book.mid_price:
|
||
logger.warning("无法获取订单簿")
|
||
return
|
||
|
||
# 检查持仓
|
||
position = self.api.get_position()
|
||
position_value = 0.0
|
||
if position:
|
||
current_price = order_book.mid_price
|
||
position_amount = abs(float(position.get('current_amount', 0)))
|
||
# 计算持仓价值(USDT)
|
||
position_value = position_amount * current_price
|
||
|
||
# 如果持仓超过限制,只挂反向单
|
||
if position_value >= self.config.max_position_usdt:
|
||
logger.warning(f"持仓超过限制: {position_value} USDT,只挂反向单")
|
||
# 只挂反向单平仓
|
||
if position:
|
||
position_type = int(position.get('position_type', 0))
|
||
if position_type == 1: # 多仓
|
||
# 挂卖单平多
|
||
_, sell_price = self.calculate_order_prices(order_book)
|
||
if sell_price and self.trading_executor:
|
||
contract_size = self.config.order_size_usdt / sell_price / 0.01
|
||
if contract_size < 1:
|
||
contract_size = 1
|
||
self.trading_executor.place_limit_order("sell", sell_price, contract_size)
|
||
elif position_type == 2: # 空仓
|
||
# 挂买单平空
|
||
buy_price, _ = self.calculate_order_prices(order_book)
|
||
if buy_price and self.trading_executor:
|
||
contract_size = self.config.order_size_usdt / buy_price / 0.01
|
||
if contract_size < 1:
|
||
contract_size = 1
|
||
self.trading_executor.place_limit_order("buy", buy_price, contract_size)
|
||
return
|
||
|
||
# 计算挂单价格
|
||
buy_price, sell_price = self.calculate_order_prices(order_book)
|
||
|
||
if not buy_price or not sell_price:
|
||
return
|
||
|
||
# 检查当前挂单数量
|
||
with self.order_lock:
|
||
pending_buy_count = sum(1 for o in self.pending_orders.values()
|
||
if o.side == "buy" and o.status == "pending")
|
||
pending_sell_count = sum(1 for o in self.pending_orders.values()
|
||
if o.side == "sell" and o.status == "pending")
|
||
|
||
# 如果两侧都有挂单,不重复挂
|
||
if pending_buy_count > 0 and pending_sell_count > 0:
|
||
return
|
||
|
||
# 挂买单(通过浏览器)
|
||
if pending_buy_count == 0:
|
||
# 计算张数(根据合约规格调整)
|
||
# 假设页面输入框单位是张数,需要将USDT金额转换为张数
|
||
# size_usdt / price = ETH数量,再除以合约面值得到张数
|
||
contract_size = self.config.order_size_usdt / buy_price / 0.01
|
||
if contract_size < 1:
|
||
contract_size = 1
|
||
|
||
if self.trading_executor and self.trading_executor.place_limit_order("buy", buy_price, contract_size):
|
||
# 使用时间戳作为订单ID
|
||
order_id = f"buy_{int(time.time() * 1000)}"
|
||
with self.order_lock:
|
||
self.pending_orders[order_id] = PendingOrder(
|
||
order_id=order_id,
|
||
side="buy",
|
||
price=buy_price,
|
||
size=self.config.order_size_usdt,
|
||
create_time=time.time(),
|
||
status="pending"
|
||
)
|
||
logger.info(f"挂买单成功: {buy_price}, 订单ID: {order_id}")
|
||
else:
|
||
logger.warning("挂买单失败")
|
||
|
||
# 挂卖单(通过浏览器)
|
||
if pending_sell_count == 0:
|
||
# 计算张数
|
||
contract_size = self.config.order_size_usdt / sell_price / 0.01
|
||
if contract_size < 1:
|
||
contract_size = 1
|
||
|
||
if self.trading_executor and self.trading_executor.place_limit_order("sell", sell_price, contract_size):
|
||
# 使用时间戳作为订单ID
|
||
order_id = f"sell_{int(time.time() * 1000)}"
|
||
with self.order_lock:
|
||
self.pending_orders[order_id] = PendingOrder(
|
||
order_id=order_id,
|
||
side="sell",
|
||
price=sell_price,
|
||
size=self.config.order_size_usdt,
|
||
create_time=time.time(),
|
||
status="pending"
|
||
)
|
||
logger.info(f"挂卖单成功: {sell_price}, 订单ID: {order_id}")
|
||
else:
|
||
logger.warning("挂卖单失败")
|
||
|
||
self.last_order_refresh = time.time()
|
||
|
||
def check_risk_limits(self) -> bool:
|
||
"""检查风险限制"""
|
||
# 检查每日交易次数
|
||
if self.daily_trades >= self.config.max_daily_trades:
|
||
logger.warning(f"达到每日最大交易次数: {self.daily_trades}")
|
||
return False
|
||
|
||
# 检查每日亏损
|
||
if self.daily_profit <= -self.config.max_daily_loss:
|
||
logger.error(f"达到每日最大亏损: {self.daily_profit}")
|
||
# send_dingtalk_message(f"做市策略达到每日最大亏损: {self.daily_profit} USDT", error=True)
|
||
return False
|
||
|
||
return True
|
||
|
||
def run(self):
|
||
"""主运行循环"""
|
||
self.running = True
|
||
logger.info("做市策略启动")
|
||
|
||
while self.running:
|
||
try:
|
||
# 检查风险限制
|
||
if not self.check_risk_limits():
|
||
logger.error("风险限制触发,停止策略")
|
||
break
|
||
|
||
# 撤销超时订单
|
||
self.cancel_stale_orders()
|
||
|
||
# 更新挂单状态
|
||
self.update_pending_orders()
|
||
|
||
# 刷新订单
|
||
if self.should_refresh_orders():
|
||
self.place_market_making_orders()
|
||
|
||
# 短暂休眠
|
||
time.sleep(0.5)
|
||
|
||
except KeyboardInterrupt:
|
||
logger.info("收到中断信号,停止策略")
|
||
break
|
||
except Exception as e:
|
||
logger.error(f"策略运行异常: {e}")
|
||
time.sleep(1)
|
||
|
||
# 清理:刷新页面,手动撤销挂单
|
||
logger.info("清理挂单...使用API撤单")
|
||
try:
|
||
self.api.cancel_all_orders()
|
||
with self.order_lock:
|
||
for order_id in list(self.pending_orders.keys()):
|
||
if self.pending_orders[order_id].status == "pending":
|
||
self.pending_orders[order_id].status = "cancelled"
|
||
except Exception as e:
|
||
logger.error(f"清理挂单失败: {e}")
|
||
|
||
logger.info("做市策略已停止")
|
||
|
||
def stop(self):
|
||
"""停止策略"""
|
||
self.running = False
|
||
|
||
|
||
# ================================================================
|
||
# 🚀 主程序
|
||
# ================================================================
|
||
|
||
if __name__ == '__main__':
|
||
config = MarketMakingConfig(
|
||
contract_symbol="ETHUSDT",
|
||
bit_id="f2320f57e24c45529a009e1541e25961", # TGE浏览器ID
|
||
trading_url="https://derivatives.bitmart.com/zh-CN/futures/ETHUSDT",
|
||
spread_percent=0.01, # 0.01%价差
|
||
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秒超时
|
||
max_daily_loss=50.0, # 每日最大亏损50 USDT
|
||
max_daily_trades=1000, # 每日最大1000笔
|
||
leverage="35",
|
||
open_type="cross"
|
||
)
|
||
|
||
strategy = MarketMakingStrategy(config)
|
||
|
||
try:
|
||
strategy.run()
|
||
except Exception as e:
|
||
logger.error(f"程序异常: {e}")
|
||
# send_dingtalk_message(f"做市策略异常: {e}", error=True)
|
||
|
||
# 9359,53
|
||
# 14.35
|