1
This commit is contained in:
304
ccxt/pro/mt5.py
304
ccxt/pro/mt5.py
@@ -41,6 +41,14 @@ class mt5(mt5Parent):
|
||||
"""监听所有数据(订单、持仓、钱包)"""
|
||||
if not hasattr(self, 'token') or not self.token:
|
||||
await self.get_token()
|
||||
|
||||
# 检查连接状态
|
||||
try:
|
||||
await self.check_connect()
|
||||
except Exception as e:
|
||||
if self.verbose:
|
||||
print(f"连接检查失败,重新连接: {e}")
|
||||
await self.get_token()
|
||||
|
||||
url = self.urls['api']['ws'] + '/OnOrderUpdate?id=' + self.token
|
||||
message_hash = 'all_data'
|
||||
@@ -153,40 +161,6 @@ class mt5(mt5Parent):
|
||||
print(f"❌ 行情监听失败: {e}")
|
||||
raise e
|
||||
|
||||
async def close_ticker(self, symbol: str, params={}):
|
||||
"""取消行情订阅"""
|
||||
await self.load_markets()
|
||||
market = self.market(symbol)
|
||||
|
||||
if not hasattr(self, 'token') or not self.token:
|
||||
await self.get_token()
|
||||
|
||||
# 取消订阅参数
|
||||
request = {
|
||||
'id': self.token,
|
||||
'symbol': market['id'],
|
||||
}
|
||||
|
||||
try:
|
||||
# 发送取消订阅请求
|
||||
result = await self.private_get('UnSubscribe', self.extend(request, params))
|
||||
|
||||
# 更新订阅状态
|
||||
if hasattr(self, 'ticker_subscriptions'):
|
||||
subscribe_hash = 'ticker:' + market['id']
|
||||
if subscribe_hash in self.ticker_subscriptions:
|
||||
self.ticker_subscriptions.remove(subscribe_hash)
|
||||
|
||||
if self.verbose:
|
||||
print(f"✅ 取消行情订阅成功: {symbol}")
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
if self.verbose:
|
||||
print(f"❌ 取消行情订阅失败: {e}")
|
||||
raise e
|
||||
|
||||
async def watch_ohlcv(self, symbol: str, timeframe='1m', since: Optional[int] = None, limit: Optional[int] = None, params={}):
|
||||
await self.load_markets()
|
||||
market = self.market(symbol)
|
||||
@@ -238,6 +212,40 @@ class mt5(mt5Parent):
|
||||
print(f"❌ K线监听失败: {e}")
|
||||
raise e
|
||||
|
||||
async def close_ticker(self, symbol: str, params={}):
|
||||
"""取消行情订阅"""
|
||||
await self.load_markets()
|
||||
market = self.market(symbol)
|
||||
|
||||
if not hasattr(self, 'token') or not self.token:
|
||||
await self.get_token()
|
||||
|
||||
# 取消订阅参数
|
||||
request = {
|
||||
'id': self.token,
|
||||
'symbol': market['id'],
|
||||
}
|
||||
|
||||
try:
|
||||
# 发送取消订阅请求
|
||||
result = await self.private_get('UnSubscribe', self.extend(request, params))
|
||||
|
||||
# 更新订阅状态
|
||||
if hasattr(self, 'ticker_subscriptions'):
|
||||
subscribe_hash = 'ticker:' + market['id']
|
||||
if subscribe_hash in self.ticker_subscriptions:
|
||||
self.ticker_subscriptions.remove(subscribe_hash)
|
||||
|
||||
if self.verbose:
|
||||
print(f"✅ 取消行情订阅成功: {symbol}")
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
if self.verbose:
|
||||
print(f"❌ 取消行情订阅失败: {e}")
|
||||
raise e
|
||||
|
||||
async def close_ohlcv(self, symbol: Optional[str] = None, timeframe: Optional[str] = None, params={}):
|
||||
"""取消K线订阅"""
|
||||
if not hasattr(self, 'token') or not self.token:
|
||||
@@ -291,29 +299,37 @@ class mt5(mt5Parent):
|
||||
if self.verbose:
|
||||
print(f"❌ 取消K线订阅失败: {e}")
|
||||
raise e
|
||||
|
||||
async def send_ticker_subscription(self, request, params={}):
|
||||
"""发送行情数据订阅请求"""
|
||||
|
||||
async def close(self):
|
||||
"""关闭所有连接和清理缓存"""
|
||||
try:
|
||||
# 使用 private_get 发送订阅
|
||||
response = await self.private_get('Subscribe', self.extend(request, params))
|
||||
# 取消所有订阅
|
||||
if hasattr(self, 'ohlcv_subscriptions') and self.ohlcv_subscriptions:
|
||||
await self.close_ohlcv()
|
||||
|
||||
if hasattr(self, 'ticker_subscriptions') and self.ticker_subscriptions:
|
||||
# 取消所有ticker订阅
|
||||
for subscribe_hash in list(self.ticker_subscriptions):
|
||||
symbol = subscribe_hash.replace('ticker:', '')
|
||||
try:
|
||||
await self.close_ticker(symbol)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 清理缓存
|
||||
self.orders_list = []
|
||||
self.positions_list = []
|
||||
self.ohlcv_cache = {}
|
||||
self.ticker_cache = {}
|
||||
|
||||
# 断开连接
|
||||
await self.disconnect()
|
||||
|
||||
if self.verbose:
|
||||
print(f"✅ 行情订阅响应: {response}")
|
||||
|
||||
# 检查订阅是否成功
|
||||
if response in [True, 'OK', 'SUCCESS']:
|
||||
return True
|
||||
else:
|
||||
raise ExchangeError(f"行情订阅失败: {response}")
|
||||
|
||||
except Exception as e:
|
||||
if self.verbose:
|
||||
print(f"private_get 失败,尝试直接HTTP请求: {e}")
|
||||
|
||||
# 直接HTTP请求
|
||||
return await self.send_direct_subscription('Subscribe', request, params)
|
||||
|
||||
print(f"关闭连接时出错: {e}")
|
||||
|
||||
await super().close()
|
||||
|
||||
def handle_message(self, client: Client, message):
|
||||
"""处理WebSocket消息 - 根据类型分发"""
|
||||
@@ -584,64 +600,75 @@ class mt5(mt5Parent):
|
||||
if not order_data:
|
||||
return None
|
||||
|
||||
symbol = self.safe_string(order_data, 'symbol')
|
||||
if symbol and len(symbol) >= 6:
|
||||
base = symbol[:3]
|
||||
quote = symbol[3:]
|
||||
symbol = base + '/' + quote
|
||||
|
||||
# 确定订单状态
|
||||
state = self.safe_string(order_data, 'state')
|
||||
status = self.parse_order_status(state)
|
||||
|
||||
# 确定订单是否已关闭
|
||||
close_time = self.safe_string(order_data, 'closeTime')
|
||||
is_closed = close_time and close_time != "0001-01-01T00:00:00"
|
||||
|
||||
timestamp = self.parse8601(self.safe_string(order_data, 'openTime'))
|
||||
last_trade_timestamp = None
|
||||
if is_closed:
|
||||
last_trade_timestamp = self.parse8601(close_time)
|
||||
|
||||
amount = self.safe_number(order_data, 'lots')
|
||||
filled = self.safe_number(order_data, 'closeLots')
|
||||
remaining = amount - filled if amount is not None and filled is not None else None
|
||||
|
||||
return {
|
||||
'id': self.safe_string(order_data, 'ticket'),
|
||||
'clientOrderId': None,
|
||||
'datetime': self.iso8601(timestamp),
|
||||
'timestamp': timestamp,
|
||||
'lastTradeTimestamp': last_trade_timestamp,
|
||||
'lastUpdateTimestamp': last_trade_timestamp,
|
||||
'status': status,
|
||||
'symbol': symbol,
|
||||
'type': self.parse_order_type(self.safe_string(order_data, 'orderType')),
|
||||
'timeInForce': None,
|
||||
'postOnly': None,
|
||||
'side': self.parse_order_side(self.safe_string(order_data, 'orderType')),
|
||||
'price': self.safe_number(order_data, 'openPrice'),
|
||||
'stopLossPrice': self.safe_number(order_data, 'stopLoss'),
|
||||
'takeProfitPrice': self.safe_number(order_data, 'takeProfit'),
|
||||
'reduceOnly':None,
|
||||
'triggerPrice': None,
|
||||
'amount': amount,
|
||||
'filled': filled,
|
||||
'remaining': remaining,
|
||||
'cost': None,
|
||||
'trades': None,
|
||||
'fee': {
|
||||
'cost': self.safe_number(order_data, 'commission', 0) + self.safe_number(order_data, 'fee', 0),
|
||||
'currency': None,
|
||||
},
|
||||
'average': None,
|
||||
'info': order_data,
|
||||
}
|
||||
try:
|
||||
symbol = self.safe_string(order_data, 'symbol')
|
||||
if symbol and len(symbol) >= 6:
|
||||
base = symbol[:3]
|
||||
quote = symbol[3:]
|
||||
symbol = base + '/' + quote
|
||||
|
||||
# 确定订单状态
|
||||
state = self.safe_string(order_data, 'state')
|
||||
status = self.parse_order_status(state)
|
||||
|
||||
# 确定订单是否已关闭
|
||||
close_time = self.safe_string(order_data, 'closeTime')
|
||||
is_closed = close_time and close_time != "0001-01-01T00:00:00"
|
||||
|
||||
timestamp = self.parse8601(self.safe_string(order_data, 'openTime'))
|
||||
if timestamp is None:
|
||||
timestamp = self.milliseconds()
|
||||
|
||||
last_trade_timestamp = None
|
||||
if is_closed:
|
||||
last_trade_timestamp = self.parse8601(close_time)
|
||||
|
||||
amount = self.safe_number(order_data, 'lots', 0)
|
||||
filled = self.safe_number(order_data, 'closeLots', 0)
|
||||
remaining = max(amount - filled, 0) if amount is not None and filled is not None else None
|
||||
|
||||
return {
|
||||
'id': self.safe_string(order_data, 'ticket'),
|
||||
'clientOrderId': None,
|
||||
'datetime': self.iso8601(timestamp),
|
||||
'timestamp': timestamp,
|
||||
'lastTradeTimestamp': last_trade_timestamp,
|
||||
'lastUpdateTimestamp': last_trade_timestamp,
|
||||
'status': status,
|
||||
'symbol': symbol,
|
||||
'type': self.parse_order_type(self.safe_string(order_data, 'orderType')),
|
||||
'timeInForce': None,
|
||||
'postOnly': None,
|
||||
'side': self.parse_order_side(self.safe_string(order_data, 'orderType')),
|
||||
'price': self.safe_number(order_data, 'openPrice'),
|
||||
'stopLossPrice': self.safe_number(order_data, 'stopLoss'),
|
||||
'takeProfitPrice': self.safe_number(order_data, 'takeProfit'),
|
||||
'reduceOnly': None,
|
||||
'triggerPrice': None,
|
||||
'amount': amount,
|
||||
'filled': filled,
|
||||
'remaining': remaining,
|
||||
'cost': None,
|
||||
'trades': None,
|
||||
'fee': {
|
||||
'cost': self.safe_number(order_data, 'commission', 0) + self.safe_number(order_data, 'fee', 0),
|
||||
'currency': None,
|
||||
},
|
||||
'average': None,
|
||||
'info': order_data,
|
||||
}
|
||||
except Exception as e:
|
||||
if self.verbose:
|
||||
print(f"解析订单数据失败: {e}")
|
||||
return None
|
||||
|
||||
def parse_ws_positions_from_orders(self, orders_data):
|
||||
"""从订单数据解析持仓信息"""
|
||||
positions = []
|
||||
|
||||
if not orders_data:
|
||||
return positions
|
||||
|
||||
if not isinstance(orders_data, list):
|
||||
orders_data = [orders_data]
|
||||
|
||||
@@ -706,6 +733,10 @@ class mt5(mt5Parent):
|
||||
"""解析WebSocket K线数据"""
|
||||
timestamp = self.parse8601(self.safe_string(data, 'time'))
|
||||
|
||||
# 确保时间戳是毫秒
|
||||
if timestamp and timestamp < 1000000000000:
|
||||
timestamp *= 1000
|
||||
|
||||
return [
|
||||
timestamp, # 时间戳
|
||||
self.safe_number(data, 'open'), # 开盘价
|
||||
@@ -797,6 +828,57 @@ class mt5(mt5Parent):
|
||||
# 方法2:直接HTTP请求
|
||||
return await self.send_direct_subscription('SubscribeOhlc', request, params)
|
||||
|
||||
async def send_ticker_subscription(self, request, params={}):
|
||||
"""发送行情数据订阅请求"""
|
||||
try:
|
||||
# 使用 private_get 发送订阅
|
||||
response = await self.private_get('Subscribe', self.extend(request, params))
|
||||
|
||||
if self.verbose:
|
||||
print(f"✅ 行情订阅响应: {response}")
|
||||
|
||||
# 检查订阅是否成功
|
||||
if response in [True, 'OK', 'SUCCESS']:
|
||||
return True
|
||||
else:
|
||||
raise ExchangeError(f"行情订阅失败: {response}")
|
||||
|
||||
except Exception as e:
|
||||
if self.verbose:
|
||||
print(f"private_get 失败,尝试直接HTTP请求: {e}")
|
||||
|
||||
# 直接HTTP请求
|
||||
return await self.send_direct_subscription('Subscribe', request, params)
|
||||
|
||||
async def send_direct_subscription(self, endpoint, request, params={}):
|
||||
"""直接发送订阅HTTP请求"""
|
||||
import aiohttp
|
||||
|
||||
# 构建完整的URL
|
||||
base_url = self.urls['api']['private']
|
||||
query_params = self.extend(request, params)
|
||||
query_string = urllib.parse.urlencode(query_params)
|
||||
|
||||
url = f"{base_url}/{endpoint}?{query_string}"
|
||||
|
||||
if self.verbose:
|
||||
print(f"🔧 直接订阅URL: {url}")
|
||||
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(url) as response:
|
||||
if response.status == 200:
|
||||
result = await response.text()
|
||||
if self.verbose:
|
||||
print(f"✅ 直接订阅成功: {result}")
|
||||
return self.parse_json(result)
|
||||
else:
|
||||
error_text = await response.text()
|
||||
raise ExchangeError(f"订阅请求失败 {response.status}: {error_text}")
|
||||
|
||||
except Exception as e:
|
||||
raise ExchangeError(f"订阅请求错误: {e}")
|
||||
|
||||
def handle_error_message(self, client: Client, message):
|
||||
"""处理错误消息"""
|
||||
error_code = self.safe_string(message, 'errorCode')
|
||||
@@ -820,7 +902,15 @@ class mt5(mt5Parent):
|
||||
except Exception as e:
|
||||
if self.verbose:
|
||||
print(f"拒绝K线消息哈希失败 {ohlcv_hash}: {e}")
|
||||
|
||||
# 拒绝所有ticker消息哈希
|
||||
if hasattr(client, 'futures') and client.futures:
|
||||
ticker_hashes = [hash for hash in client.futures.keys() if isinstance(hash, str) and hash.startswith('ticker:')]
|
||||
for ticker_hash in ticker_hashes:
|
||||
try:
|
||||
client.reject(error, ticker_hash)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
except Exception as e:
|
||||
if self.verbose:
|
||||
print(f"错误处理失败: {e}")
|
||||
Reference in New Issue
Block a user