Files
ccxt_with_mt5/ccxt/pro/mt5.py
lz_db be4c883d62 1
2025-12-02 15:17:58 +08:00

950 lines
37 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.
# -*- coding: utf-8 -*-
from ccxt.async_support.mt5 import mt5 as mt5Parent
from ccxt.base.errors import ExchangeError, ArgumentsRequired
from ccxt.async_support.base.ws.client import Client
import asyncio
from typing import Optional, Dict, Any, List
import urllib.parse
class mt5(mt5Parent):
def describe(self):
return self.deep_extend(super(mt5, self).describe(), {
'has': {
'ws': True,
'watchBalance': True,
'watchOrders': True,
'watchPositions': True,
'watchTicker': True,
'watchOHLCV': True,
'watchAll': True,
},
'urls': {
'api': {
'ws': 'ws://{hostname}',
},
},
'options': {
'tradesLimit': 1000,
'ordersLimit': 1000,
'OHLCVLimit': 1000,
'positionsLimit': 1000,
},
'streaming': {
'ping': True,
'maxPingPongMisses': 2,
},
})
async def watch_all(self, params={}):
"""监听所有数据(订单、持仓、钱包)"""
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()
ws_url = self.implode_hostname(self.urls['api']['ws'])
url = ws_url + '/OnOrderUpdate?id=' + self.token
message_hash = 'all_data'
return await self.watch(url, message_hash)
async def watch_balance(self, params={}):
"""监听余额变化"""
if not hasattr(self, 'token') or not self.token:
await self.get_token()
ws_url = self.implode_hostname(self.urls['api']['ws'])
url = ws_url + '/OnOrderUpdate?id=' + self.token
message_hash = 'balance'
return await self.watch(url, message_hash)
async def watch_orders(self, symbol: Optional[str] = None, since: Optional[int] = None, limit: int = 1, params={}):
"""监听订单变化"""
if not hasattr(self, 'token') or not self.token:
await self.get_token()
await self.server_timezone()
ws_url = self.implode_hostname(self.urls['api']['ws'])
url = ws_url + '/OnOrderUpdate?id=' + self.token
message_hash = 'orders'
if symbol is not None:
symbol = self.symbol(symbol)
message_hash += ':' + symbol
orders = await self.watch(url, message_hash)
# 过滤订单
if symbol is not None:
orders = [order for order in orders if order['symbol'] == symbol]
if since is not None:
orders = [order for order in orders if order['timestamp'] >= since]
# 限制返回数量
if limit is not None:
orders = orders[-limit:]
return orders
async def watch_positions(self, symbols: Optional[List[str]] = None, since: Optional[int] = None, limit: Optional[int] = None, params={}):
"""监听持仓变化"""
if not hasattr(self, 'token') or not self.token:
await self.get_token()
await self.server_timezone()
ws_url = self.implode_hostname(self.urls['api']['ws'])
url = ws_url + '/OnOrderUpdate?id=' + self.token
message_hash = 'positions'
if symbols is not None:
symbols = [self.symbol(symbol) for symbol in symbols]
message_hash += ':' + ':'.join(symbols)
positions = await self.watch(url, message_hash)
# 过滤持仓
if symbols is not None:
positions = [position for position in positions if position['symbol'] in symbols]
if since is not None:
positions = [position for position in positions if position['timestamp'] >= since]
if limit is not None:
positions = positions[-limit:]
return positions
async def watch_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()
# WebSocket 监听地址
ws_url = self.implode_hostname(self.urls['api']['ws'])
url = ws_url + '/OnQuote?id=' + self.token
# 订阅参数
request = {
'id': self.token,
'symbol': market['id'],
}
# 消息哈希
message_hash = 'ticker:' + market['symbol']
# 订阅哈希(用于管理订阅状态)
subscribe_hash = 'ticker:' + market['id']
try:
# 检查是否已经订阅
if not hasattr(self, 'ticker_subscriptions'):
self.ticker_subscriptions = set()
# 如果尚未订阅,发送订阅请求
if subscribe_hash not in self.ticker_subscriptions:
subscription_result = await self.send_ticker_subscription(request, params)
if not subscription_result:
raise ExchangeError("行情订阅失败")
# 记录订阅状态
self.ticker_subscriptions.add(subscribe_hash)
if self.verbose:
print(f"✅ 行情订阅成功: {symbol}")
# 监听 WebSocket 数据
ticker = await self.watch(url, message_hash, subscribe_hash=subscribe_hash)
return ticker
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)
if not hasattr(self, 'token') or not self.token:
await self.get_token()
# WebSocket 监听地址
ws_url = self.implode_hostname(self.urls['api']['ws'])
url = ws_url + '/OnOhlc?id=' + self.token
# 订阅参数
request = {
'id': self.token,
'symbol': market['id'],
'timeframe': self.timeframes[timeframe],
}
# 消息哈希
message_hash = 'ohlcv:' + market['symbol'] + ':' + timeframe
# 订阅哈希(用于管理订阅状态)
subscribe_hash = 'ohlcv:' + market['id'] + ':' + str(self.timeframes[timeframe])
try:
# 检查是否已经订阅
if not hasattr(self, 'ohlcv_subscriptions'):
self.ohlcv_subscriptions = set()
# 如果尚未订阅,发送订阅请求
if subscribe_hash not in self.ohlcv_subscriptions:
subscription_result = await self.send_ohlcv_subscription(request, params)
if not subscription_result:
raise ExchangeError("K线订阅失败")
# 记录订阅状态
self.ohlcv_subscriptions.add(subscribe_hash)
if self.verbose:
print(f"✅ K线订阅成功: {symbol} {timeframe}")
# 监听 WebSocket 数据
ohlcv = await self.watch(url, message_hash, subscribe_hash=subscribe_hash)
return self.filter_by_since_limit(ohlcv, since, limit, 0, True)
except Exception as e:
if self.verbose:
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:
await self.get_token()
# 取消订阅参数
request = {
'id': self.token,
}
# 如果指定了交易对,添加到参数中
if symbol is not None:
await self.load_markets()
market = self.market(symbol)
request['symbol'] = market['id']
try:
# 发送取消订阅请求
result = await self.private_get('UnsubscribeOhlc', self.extend(request, params))
# 更新订阅状态
if hasattr(self, 'ohlcv_subscriptions'):
if symbol is not None:
# 取消指定交易对的所有时间帧订阅
if timeframe is not None:
# 取消指定交易对和指定时间帧的订阅
subscribe_hash = 'ohlcv:' + market['id'] + ':' + str(self.timeframes[timeframe])
if subscribe_hash in self.ohlcv_subscriptions:
self.ohlcv_subscriptions.remove(subscribe_hash)
else:
# 取消指定交易对的所有时间帧订阅
to_remove = [h for h in self.ohlcv_subscriptions if h.startswith('ohlcv:' + market['id'] + ':')]
for hash_to_remove in to_remove:
self.ohlcv_subscriptions.remove(hash_to_remove)
else:
# 取消所有K线订阅
self.ohlcv_subscriptions.clear()
if self.verbose:
if symbol is not None:
if timeframe is not None:
print(f"✅ 取消K线订阅成功: {symbol} {timeframe}")
else:
print(f"✅ 取消K线订阅成功: {symbol} (所有时间帧)")
else:
print(f"✅ 取消所有K线订阅成功")
return result
except Exception as e:
if self.verbose:
print(f"❌ 取消K线订阅失败: {e}")
raise e
async def close(self):
"""关闭所有连接和清理缓存"""
try:
# 取消所有订阅
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()
except Exception as e:
if self.verbose:
print(f"关闭连接时出错: {e}")
await super().close()
def handle_message(self, client: Client, message):
"""处理WebSocket消息 - 根据类型分发"""
try:
# 检查错误
error_code = self.safe_string(message, 'errorCode')
if error_code is not None and error_code != '0':
self.handle_error_message(client, message)
return
# 解析消息类型
message_type = self.safe_string(message, 'type')
if message_type == 'OrderUpdate':
self.handle_order_update_message(client, message)
elif message_type == 'OpenedOrders':
self.handle_opened_orders_message(client, message)
elif message_type == 'Ohlc':
self.handle_ohlc_message(client, message)
elif message_type == 'Quote':
self.handle_ticker_message(client, message)
else:
if self.verbose:
print(f"未知消息类型: {message_type}")
except Exception as e:
if self.verbose:
print(f"处理WebSocket消息失败: {e}, message: {message}")
def handle_order_update_message(self, client: Client, message):
"""处理 OrderUpdate 类型消息(包含订单、持仓、余额)"""
try:
data = self.safe_value(message, 'data', {})
timestamp = self.safe_integer(message, 'timestampUTC') - self.diff_milliseconds
# 1. 解析余额信息
balance_data = self.parse_ws_balance_from_data(data)
if balance_data:
balance_data['timestamp'] = timestamp
balance_data['datetime'] = self.iso8601(timestamp)
self.balance = balance_data
client.resolve(balance_data, 'balance')
else:
# 即使没有余额数据,也返回空余额
client.resolve(self.safe_balance({}), 'balance')
# 2. 解析订单信息(从 update.order
update_data = self.safe_value(data, 'update', {})
order_data = self.safe_value(update_data, 'order')
if order_data:
order_data['update_type'] = self.safe_value(update_data, 'type', None) # 这个字段可以判断是开仓还是平仓
order = self.parse_ws_order(order_data)
if order:
# 使用简单的列表而不是 ArrayCacheBySymbolById
if not hasattr(self, 'orders_list'):
self.orders_list = []
# 更新或添加订单
self.update_or_add_order(order)
client.resolve(self.orders_list.copy(), 'orders')
else:
# 即使没有订单数据,也返回空列表
if not hasattr(self, 'orders_list'):
self.orders_list = []
client.resolve(self.orders_list.copy(), 'orders')
# 3. 解析持仓信息(从 openedOrders
opened_orders = self.safe_value(data, 'openedOrders', [])
positions = self.parse_ws_positions_from_orders(opened_orders)
# 总是返回持仓列表,即使为空
if not hasattr(self, 'positions_list'):
self.positions_list = []
# 完全替换持仓列表
self.positions_list = positions
client.resolve(self.positions_list.copy(), 'positions')
# 4. 返回所有数据(用于 watch_all
all_data = {
'balance': balance_data if balance_data else self.safe_balance({}),
'orders': self.orders_list.copy() if hasattr(self, 'orders_list') else [],
'positions': self.positions_list.copy() if hasattr(self, 'positions_list') else [],
'timestamp': timestamp,
}
client.resolve(all_data, 'all_data')
except Exception as e:
error = ExchangeError(f"解析OrderUpdate消息失败: {e}")
client.reject(error, 'balance')
client.reject(error, 'orders')
client.reject(error, 'positions')
client.reject(error, 'all_data')
def handle_opened_orders_message(self, client: Client, message):
"""处理 OpenedOrders 类型消息(只包含持仓)"""
try:
data = self.safe_value(message, 'data', [])
timestamp = self.safe_integer(message, 'timestampUTC') - self.diff_milliseconds
# 解析持仓信息
positions = self.parse_ws_positions_from_orders(data)
# 总是返回持仓列表,即使为空
if not hasattr(self, 'positions_list'):
self.positions_list = []
# 完全替换持仓列表
self.positions_list = positions
client.resolve(self.positions_list.copy(), 'positions')
# 返回所有数据(持仓数据)
all_data = {
'balance': None,
'orders': [],
'positions': self.positions_list.copy() if hasattr(self, 'positions_list') else [],
'timestamp': timestamp,
}
client.resolve(all_data, 'all_data')
except Exception as e:
error = ExchangeError(f"解析OpenedOrders消息失败: {e}")
client.reject(error, 'positions')
client.reject(error, 'all_data')
def handle_ohlc_message(self, client: Client, message):
"""处理K线数据消息"""
try:
data = self.safe_value(message, 'data', {})
symbol = self.safe_string(data, 'symbol')
if symbol and len(symbol) >= 6:
base = symbol[:3]
quote = symbol[3:]
symbol = base + '/' + quote
# 获取时间帧
timeframe_value = self.safe_integer(data, 'timeframe')
timeframe = self.parse_timeframe_from_value(timeframe_value)
# 构建消息哈希
message_hash = 'ohlcv:' + symbol + ':' + timeframe
# 解析K线数据
ohlcv = self.parse_ws_ohlcv_message(data)
# 更新缓存
if not hasattr(self, 'ohlcv_cache'):
self.ohlcv_cache = {}
if symbol not in self.ohlcv_cache:
self.ohlcv_cache[symbol] = {}
stored = self.ohlcv_cache[symbol].get(timeframe)
if stored is None:
limit = self.safe_integer(self.options, 'OHLCVLimit', 1000)
# 使用简单的列表代替 ArrayCacheByTimestamp
stored = []
self.ohlcv_cache[symbol][timeframe] = stored
# 添加新的K线数据
stored.append(ohlcv)
# 限制缓存大小
if len(stored) > self.options['OHLCVLimit']:
stored = stored[-self.options['OHLCVLimit']:]
self.ohlcv_cache[symbol][timeframe] = stored
# 返回数据
client.resolve(stored, message_hash)
except Exception as e:
if self.verbose:
print(f"处理K线消息失败: {e}")
# 拒绝相关的promise
symbol = self.safe_string(message.get('data', {}), 'symbol')
if symbol and len(symbol) >= 6:
base = symbol[:3]
quote = symbol[3:]
symbol = base + '/' + quote
timeframe_value = self.safe_integer(message.get('data', {}), 'timeframe')
timeframe = self.parse_timeframe_from_value(timeframe_value)
message_hash = 'ohlcv:' + symbol + ':' + timeframe
client.reject(e, message_hash)
def handle_ticker_message(self, client: Client, message):
"""处理行情数据消息"""
try:
data = self.safe_value(message, 'data', {})
symbol = self.safe_string(data, 'symbol')
if symbol and len(symbol) >= 6:
base = symbol[:3]
quote = symbol[3:]
symbol = base + '/' + quote
# 构建消息哈希
message_hash = 'ticker:' + symbol
# 解析行情数据
ticker = self.parse_ws_ticker_message(data)
# 更新缓存
if not hasattr(self, 'ticker_cache'):
self.ticker_cache = {}
self.ticker_cache[symbol] = ticker
# 返回数据
client.resolve(ticker, message_hash)
except Exception as e:
if self.verbose:
print(f"处理行情消息失败: {e}")
# 拒绝相关的promise
symbol = self.safe_string(message.get('data', {}), 'symbol')
if symbol and len(symbol) >= 6:
base = symbol[:3]
quote = symbol[3:]
symbol = base + '/' + quote
message_hash = 'ticker:' + symbol
client.reject(e, message_hash)
def update_or_add_order(self, order):
"""更新或添加订单到列表"""
if not hasattr(self, 'orders_list'):
self.orders_list = []
order_id = order['id']
# 查找是否已存在相同ID的订单
existing_index = -1
for i, existing_order in enumerate(self.orders_list):
if existing_order['id'] == order_id:
existing_index = i
break
if existing_index >= 0:
# 更新现有订单
self.orders_list[existing_index] = order
else:
# 添加新订单
self.orders_list.append(order)
# 限制订单列表长度
if len(self.orders_list) > self.options['ordersLimit']:
self.orders_list = self.orders_list[-self.options['ordersLimit']:]
def parse_ws_balance_from_data(self, data):
"""从数据中解析余额信息"""
result = {
'timestamp': None,
'datetime': None,
}
currency = 'USDT'
balance = self.safe_number(data, 'balance', 0)
equity = self.safe_number(data, 'equity', 0)
margin = self.safe_number(data, 'margin', 0)
free_margin = self.safe_number(data, 'freeMargin', 0)
profit = self.safe_number(data, 'profit', 0)
result[currency] = {
'free': free_margin,
'used': margin,
'total': balance,
'equity': equity,
'profit': profit,
}
return self.safe_balance(result)
def parse_ws_order(self, order_data):
"""解析单个订单数据"""
if not order_data:
return None
# print("++++++",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.safe_integer(order_data, 'openTimestampUTC') - self.diff_milliseconds
last_trade_timestamp = self.safe_integer(order_data, 'closeTimestampUTC')
if last_trade_timestamp is None or last_trade_timestamp <= 0:
last_trade_timestamp = timestamp
else:
last_trade_timestamp = last_trade_timestamp - self.diff_milliseconds
mt5_order_type = self.safe_string(order_data, 'update_type', None)
amount = self.safe_number(order_data, 'lots', 0)
filled = self.safe_number(order_data, 'closeLots', 0)
price = self.safe_number(order_data, 'openPrice')
side = self.parse_order_side(self.safe_string(order_data, 'orderType'))
if mt5_order_type == 'MarketOpen':
amount = self.safe_number(order_data, 'lots', 0)
filled = self.safe_number(order_data, 'lots', 0)
elif mt5_order_type == 'MarketClose':
amount = self.safe_number(self.safe_dict(order_data, 'dealInternalIn', {}), 'lots', 0)
filled = self.safe_number(order_data, 'closeLots', 0)
price = self.safe_number(order_data, 'closePrice')
if side == 'buy':
side = 'sell'
else:
side = 'buy'
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': self.safe_string(order_data, 'comment'),
'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': side,
'price': price,
'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]
for order_data in orders_data:
try:
# 只有状态为 Filled 的订单才被认为是持仓
state = self.safe_string(order_data, 'state')
if state == 'Filled':
position = self.parse_ws_position(order_data)
if position:
positions.append(position)
except Exception as e:
if self.verbose:
print(f"解析持仓失败: {e}")
continue
return positions
def parse_ws_position(self, order_data):
"""解析单个持仓"""
symbol = self.safe_string(order_data, 'symbol')
if symbol and len(symbol) >= 6:
base = symbol[:3]
quote = symbol[3:]
symbol = base + '/' + quote
timestamp = self.safe_integer(order_data, 'openTimestampUTC') - self.diff_milliseconds
contracts = self.safe_number(order_data, 'lots')
entry_price = self.safe_number(order_data, 'openPrice')
mark_price = self.safe_number(order_data, 'closePrice')
notional = contracts * entry_price if contracts and entry_price else None
# 获取止损止盈价格
stop_loss_price = self.safe_number(order_data, 'stopLoss')
take_profit_price = self.safe_number(order_data, 'takeProfit')
# 确定持仓方向
order_type = self.safe_string(order_data, 'orderType')
side = 'long' if order_type == 'Buy' else 'short'
return {
'id': self.safe_string(order_data, 'ticket'),
'symbol': symbol,
'timestamp': timestamp,
'datetime': self.iso8601(timestamp),
'side': side,
'contracts': contracts,
'contractSize': self.safe_number(order_data, 'contractSize', 1.0),
'entryPrice': entry_price,
'markPrice': mark_price,
'notional': notional,
'leverage': 1,
'unrealizedPnl': self.safe_number(order_data, 'profit'),
'realizedPnl': 0,
'liquidationPrice': None,
'marginMode': 'cross',
'percentage': None,
'marginRatio': None,
'collateral': None,
'initialMargin': None,
'initialMarginPercentage': None,
'maintenanceMargin': None,
'maintenanceMarginPercentage': None,
'stopLossPrice': stop_loss_price,
'takeProfitPrice': take_profit_price,
'lastPrice': mark_price,
'hedged': None,
'info': order_data,
}
def parse_ws_ohlcv_message(self, data):
"""解析WebSocket K线数据"""
timestamp = self.parse8601(self.safe_string(data, 'time'))
# 确保时间戳是毫秒
if timestamp and timestamp < 1000000000000:
timestamp *= 1000
return [
timestamp, # 时间戳
self.safe_number(data, 'open'), # 开盘价
self.safe_number(data, 'high'), # 最高价
self.safe_number(data, 'low'), # 最低价
self.safe_number(data, 'close'), # 收盘价
self.safe_number(data, 'volume', 0) # 成交量
]
def parse_ws_ticker_message(self, data):
"""解析WebSocket行情数据"""
symbol = self.safe_string(data, 'symbol')
if symbol and len(symbol) >= 6:
base = symbol[:3]
quote = symbol[3:]
symbol = base + '/' + quote
timestamp = self.parse8601(self.safe_string(data, 'time'))
bid = self.safe_number(data, 'bid')
ask = self.safe_number(data, 'ask')
last = self.safe_number(data, 'last')
# 如果没有 last 价格,使用中间价
if last is None and bid is not None and ask is not None:
last = (bid + ask) / 2
# 计算涨跌幅需要历史数据这里设为None
change = None
percentage = None
return {
'symbol': symbol,
'timestamp': timestamp,
'datetime': self.iso8601(timestamp),
'high': None,
'low': None,
'bid': bid,
'bidVolume': None,
'ask': ask,
'askVolume': None,
'vwap': None,
'open': None,
'close': None,
'last': last,
'previousClose': None,
'change': change,
'percentage': percentage,
'average': None,
'baseVolume': self.safe_number(data, 'volume'),
'quoteVolume': None,
'info': data,
}
def parse_timeframe_from_value(self, timeframe_value):
"""将时间帧数值转换为字符串"""
timeframe_map = {
1: '1m',
5: '5m',
15: '15m',
30: '30m',
60: '1h',
240: '4h',
1440: '1d',
10080: '1w',
43200: '1M'
}
return self.safe_string(timeframe_map, timeframe_value, '1m')
async def send_ohlcv_subscription(self, request, params={}):
"""发送K线数据订阅请求"""
try:
# 方法1尝试使用 private_get
response = await self.private_get('SubscribeOhlc', self.extend(request, params))
if self.verbose:
print(f"✅ K线订阅成功: {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}")
# 方法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')
error_message = self.safe_string(message, 'message', 'Unknown error')
error = ExchangeError(f"MT5 WebSocket error {error_code}: {error_message}")
try:
# 拒绝固定消息哈希
client.reject(error, 'balance')
client.reject(error, 'orders')
client.reject(error, 'positions')
client.reject(error, 'all_data')
# 安全地拒绝所有K线消息哈希
if hasattr(client, 'futures') and client.futures:
ohlcv_hashes = [hash for hash in client.futures.keys() if isinstance(hash, str) and hash.startswith('ohlcv:')]
for ohlcv_hash in ohlcv_hashes:
try:
client.reject(error, ohlcv_hash)
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}")