Files
ccxt_with_mt5/ccxt/pro/mt5.py
lz_db 42a0391eeb 1
2025-11-17 17:18:51 +08:00

826 lines
31 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://43.167.188.220:5000',
},
},
'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()
url = self.urls['api']['ws'] + '/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()
url = self.urls['api']['ws'] + '/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()
url = self.urls['api']['ws'] + '/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()
url = self.urls['api']['ws'] + '/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 监听地址
url = self.urls['api']['ws'] + '/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 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)
if not hasattr(self, 'token') or not self.token:
await self.get_token()
# WebSocket 监听地址
url = self.urls['api']['ws'] + '/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_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 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)
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')
# 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 = 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')
# 解析持仓信息
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
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,
}
def parse_ws_positions_from_orders(self, orders_data):
"""从订单数据解析持仓信息"""
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.parse8601(self.safe_string(order_data, 'openTime'))
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
return {
'id': self.safe_string(order_data, 'ticket'),
'symbol': symbol,
'timestamp': timestamp,
'datetime': self.iso8601(timestamp),
'side': self.parse_order_side(self.safe_string(order_data, 'orderType')),
'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,
'info': order_data,
}
def parse_ws_ohlcv_message(self, data):
"""解析WebSocket K线数据"""
timestamp = self.parse8601(self.safe_string(data, 'time'))
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)
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}")
except Exception as e:
if self.verbose:
print(f"错误处理失败: {e}")