From da459da0f3ad4fddcf49d29e924b5bd874a2f270 Mon Sep 17 00:00:00 2001 From: lz_db Date: Mon, 17 Nov 2025 19:36:28 +0800 Subject: [PATCH] 1 --- ccxt/async_support/mt5.py | 94 ++++++++++-- ccxt/mt5.py | 73 +++------ ccxt/pro/mt5.py | 304 ++++++++++++++++++++++++-------------- test/atest_mt5_quick.py | 41 ++--- 4 files changed, 324 insertions(+), 188 deletions(-) diff --git a/ccxt/async_support/mt5.py b/ccxt/async_support/mt5.py index 59ece59..9d136d0 100644 --- a/ccxt/async_support/mt5.py +++ b/ccxt/async_support/mt5.py @@ -1,8 +1,5 @@ # -*- coding: utf-8 -*- -# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: -# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code - from ccxt.async_support.base.exchange import Exchange from ccxt.abstract.mt5 import ImplicitAPI import asyncio @@ -57,8 +54,7 @@ class mt5(Exchange, ImplicitAPI): 'fetchOrder': True, 'fetchOrderBook': True, 'fetchTicker': True, - 'fetchTickers': True, # 添加这个字段 - 'fetchTickers': True, + 'fetchTickers': True, }, 'timeframes': { '1m': 1, @@ -166,6 +162,19 @@ class mt5(Exchange, ImplicitAPI): } return await self.private_get_checkconnect(request) + async def disconnect(self): + """断开连接""" + if hasattr(self, 'token') and self.token: + request = { + 'id': self.token, + } + try: + await self.private_get_disconnect(request) + except Exception: + pass + finally: + self.token = None + async def fetch_markets(self, params={}): """获取交易对列表 - 异步修复版本""" if not hasattr(self, 'token') or not self.token: @@ -225,7 +234,12 @@ class mt5(Exchange, ImplicitAPI): # 确保符号格式正确 (如 EURUSD) if len(symbol) < 6: - return None + # 处理较短的符号 + base = symbol + quote = 'USD' # 默认报价货币 + else: + base = symbol[:3] + quote = symbol[3:] base = symbol[:3] quote = symbol[3:] @@ -278,6 +292,21 @@ class mt5(Exchange, ImplicitAPI): print(f"解析市场信息失败: {e}, info: {info}") return None + async def server_timezone(self): + """获得mt5服务器时区""" + if hasattr(self, 'timezone'): + return self.timezone + else: + if not hasattr(self, 'token') or not self.token: + await self.get_token() + + request = { + 'id': self.token, + } + response = await self.private_get_servertimezone(request) + self.timezone = int(float(response)) + return self.timezone + async def fetch_balance(self, params={}): """获取账户余额""" if not hasattr(self, 'token') or not self.token: @@ -302,16 +331,49 @@ class mt5(Exchange, ImplicitAPI): margin = self.safe_number(response, 'margin', 0.0) free_margin = self.safe_number(response, 'freeMargin', 0.0) equity = self.safe_number(response, 'equity', 0.0) + profit = self.safe_number(response, 'profit', 0.0) result[currency] = { 'free': free_margin, 'used': margin, 'total': balance, 'equity': equity, + 'profit': profit, } return self.safe_balance(result) + async def fetch_account_details(self, params={}): + """获取账户信息""" + if not hasattr(self, 'token') or not self.token: + await self.get_token() + + request = { + 'id': self.token, + } + response = await self.private_get_accountdetails(self.extend(request, params)) + + return self.parse_account(response) + + def parse_account(self, response): + """解析账户信息""" + return { + 'serverName': self.safe_string(response, 'serverName'), + 'user': self.safe_string(response, 'user'), + 'host': self.safe_string(response, 'host'), + 'port': self.safe_integer(response, 'port'), + 'serverTime': self.safe_string(response, 'serverTime'), + 'serverTimeZone': self.safe_integer(response, 'serverTimeZone'), + 'company': self.safe_string(response, 'company'), + 'currency': self.safe_string(response, 'currency', 'UST'), + 'accountName': self.safe_string(response, 'accountName'), + 'group': self.safe_string(response, 'group'), + 'accountType': self.safe_string(response, 'accountType'), + 'accountLeverage': self.safe_integer(response, 'accountLeverage'), + 'accountMethod': self.safe_string(response, 'accountMethod'), + 'isInvestor': self.safe_value(response, 'isInvestor', False), + } + async def fetch_ticker(self, symbol, params={}): """获取行情数据""" await self.load_markets() @@ -531,6 +593,9 @@ class mt5(Exchange, ImplicitAPI): 'cost': fee_cost, 'currency': None, } + # 确保所有必需字段都有值 + if timestamp is None: + timestamp = self.milliseconds() return self.safe_order({ 'id': id, @@ -556,13 +621,21 @@ class mt5(Exchange, ImplicitAPI): 'cost': cost, 'trades': None, 'fee': fee, - # 'info': order, + 'info': order, 'average': None, }) except Exception as e: if self.verbose: print(f"解析订单失败: {e}, order: {order}") - raise e + return self.safe_order({ + 'id': self.safe_string(order, 'ticket'), + 'symbol': symbol, + 'status': 'unknown', + 'side': 'unknown', + 'type': 'unknown', + 'timestamp': self.milliseconds(), + 'info': order, + }) def parse_order_status(self, status): statuses = { @@ -739,14 +812,15 @@ class mt5(Exchange, ImplicitAPI): import aiohttp try: async with aiohttp.ClientSession() as session: - async with session.get(url) as response: + async with session.get(url, timeout=aiohttp.ClientTimeout(total=30)) as response: if response.status == 200: content = await response.text() - # 直接返回内容,让 parse_json 处理 return self.parse_json(content) else: error_text = await response.text() raise ExchangeError(f"HTTP {response.status}: {error_text}") + except asyncio.TimeoutError: + raise ExchangeError("请求超时") except Exception as e: raise ExchangeError(f"请求失败: {e}") diff --git a/ccxt/mt5.py b/ccxt/mt5.py index 1f6c501..6c080d5 100644 --- a/ccxt/mt5.py +++ b/ccxt/mt5.py @@ -1,8 +1,5 @@ # -*- coding: utf-8 -*- -# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: -# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code - from ccxt.base.exchange import Exchange from ccxt.abstract.mt5 import ImplicitAPI from ccxt.base.types import Any, Balances, BorrowInterest, Conversion, CrossBorrowRate, Currencies, Currency, DepositAddress, FundingHistory, Greeks, Int, LedgerEntry, Leverage, LeverageTier, LeverageTiers, Liquidation, LongShortRatio, Market, Num, Option, OptionChain, Order, OrderBook, OrderRequest, CancellationRequest, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, FundingRate, FundingRates, Trade, TradingFeeInterface, TradingFees, Transaction, MarketInterface, TransferEntry @@ -79,7 +76,7 @@ class mt5(Exchange, ImplicitAPI): 'urls': { 'logo': '', 'api': { - 'public': 'http://43.167.188.220:5000', # 直接使用具体地址,不使用 {hostname} + 'public': 'http://43.167.188.220:5000', 'private': 'http://43.167.188.220:5000', }, 'www': 'http://43.167.188.220:5000', @@ -112,7 +109,10 @@ class mt5(Exchange, ImplicitAPI): 'OrderSend': 1, 'OrderModify': 1, 'OrderClose': 1, - 'SubscribeOhlc': 1, # 添加订阅端点 + 'SubscribeOhlc': 1, + 'UnsubscribeOhlc': 1, + 'Subscribe': 1, + 'UnSubscribe': 1, }, }, }, @@ -157,10 +157,10 @@ class mt5(Exchange, ImplicitAPI): def connect(self): """连接到 MT5 账户并获取 token""" params = { - 'user': self.apiKey, # apiKey 作为 user - 'password': self.secret, # secret 作为 password - 'host': self.options['host'], # 账号所分配在的MT5服务器IP - 'port': self.options['port'], # 账号所分配在的MT5服务器端口 + 'user': self.apiKey, + 'password': self.secret, + 'host': self.options['host'], + 'port': self.options['port'], 'connectTimeoutSeconds': self.options['connectTimeoutSeconds'], } @@ -225,7 +225,6 @@ class mt5(Exchange, ImplicitAPI): if market and market.get('symbol'): markets.append(market) except Exception as e: - # 跳过解析失败的市场,继续处理其他市场 if self.verbose: print(f"跳过交易对 {symbol}: {e}") continue @@ -252,7 +251,6 @@ class mt5(Exchange, ImplicitAPI): def parse_market(self, info): """解析市场信息 - 更健壮的版本""" try: - # 安全获取 symbol if not isinstance(info, dict): return None @@ -262,21 +260,15 @@ class mt5(Exchange, ImplicitAPI): symbol = symbol.upper().strip() - # 确保符号格式正确 (如 EURUSD, BTCUSD) + # 处理符号格式 if len(symbol) < 6: - # 处理较短的符号(如黄金 XAUUSD 是6位,但可能有其他情况) - # 对于无法确定格式的符号,直接使用原始符号 base = symbol - quote = 'USD' # 默认报价货币 + quote = 'USD' else: - # 假设标准格式是3位基础货币+3位报价货币 base = symbol[:3] quote = symbol[3:] - # 安全处理精度 digits = self.safe_integer(info, 'digits', 5) - - # 确保 digits 是整数 if digits is not None: try: digits = int(digits) @@ -374,9 +366,7 @@ class mt5(Exchange, ImplicitAPI): 'id': self.token, } - # 如果指定了特定的交易对 if symbols is not None: - # 将符号列表转换为 MT5 格式(如 ['EUR/USD', 'GBP/USD'] -> ['EURUSD', 'GBPUSD']) mt5_symbols = [] for symbol in symbols: market = self.market(symbol) @@ -387,7 +377,6 @@ class mt5(Exchange, ImplicitAPI): response = self.private_get_getquotemany(self.extend(request, params)) return self.parse_tickers(response, symbols) except Exception as e: - # 如果批量获取失败,回退到逐个获取 if symbols is not None: return self.fetch_tickers_fallback(symbols, params) else: @@ -411,7 +400,6 @@ class mt5(Exchange, ImplicitAPI): tickers = {} if isinstance(response, list): - # 如果响应是数组 for ticker_data in response: try: ticker = self.parse_ticker(ticker_data) @@ -422,7 +410,6 @@ class mt5(Exchange, ImplicitAPI): print(f"解析行情数据失败: {e}") continue elif isinstance(response, dict): - # 如果响应是字典 for symbol_key, ticker_data in response.items(): try: ticker = self.parse_ticker(ticker_data) @@ -433,7 +420,6 @@ class mt5(Exchange, ImplicitAPI): print(f"解析行情数据失败 {symbol_key}: {e}") continue - # 如果指定了特定的交易对,确保返回的顺序一致 if symbols is not None: ordered_tickers = {} for symbol in symbols: @@ -453,8 +439,6 @@ class mt5(Exchange, ImplicitAPI): } response = self.private_get_getquote(self.extend(request, params)) - # MT5 的 GetQuote 返回的是最新报价,不是完整的订单簿 - # 这里模拟一个简单的订单簿 bid = self.safe_number(response, 'bid') ask = self.safe_number(response, 'ask') @@ -485,7 +469,7 @@ class mt5(Exchange, ImplicitAPI): 'datetime': None, } - currency = 'USDT' # 强制设定为 USDT + currency = 'USDT' balance = self.safe_number(response, 'balance', 0.0) margin = self.safe_number(response, 'margin', 0.0) free_margin = self.safe_number(response, 'freeMargin', 0.0) @@ -530,7 +514,7 @@ class mt5(Exchange, ImplicitAPI): } def fetch_open_orders(self, symbol=None, since=None, limit=None, params={}): - """获取未平仓订单 - 修复版本""" + """获取未平仓订单""" self.load_token() request = { 'id': self.token, @@ -538,7 +522,6 @@ class mt5(Exchange, ImplicitAPI): response = self.private_get_openedorders(self.extend(request, params)) - # 如果指定了特定交易对,进行过滤 if symbol is not None: market = self.market(symbol) filtered_orders = [] @@ -550,15 +533,14 @@ class mt5(Exchange, ImplicitAPI): return self.parse_orders(response, None, since, limit) def fetch_closed_orders(self, symbol=None, since=None, limit=None, params={}): - """获取已平仓订单 - 修复版本""" + """获取已平仓订单""" self.load_token() request = { 'id': self.token, } response = self.private_get_closedorders(self.extend(request, params)) - # print(response) - # 如果指定了特定交易对,进行过滤 + if symbol is not None: market = self.market(symbol) filtered_orders = [] @@ -570,25 +552,20 @@ class mt5(Exchange, ImplicitAPI): return self.parse_orders(response, None, since, limit) def parse_order(self, order, market=None): - """解析订单信息 - 修复市场符号问题""" + """解析订单信息""" try: id = self.safe_string(order, 'ticket') market_id = self.safe_string(order, 'symbol') - # 安全地解析市场符号 symbol = None if market is not None: symbol = market['symbol'] elif market_id is not None: - # 修复:提供更多参数来正确解析符号 - # 假设 MT5 的符号格式是 BASEQUOTE(如 EURUSD, BTCUSD) if len(market_id) >= 6: - # 尝试解析为 3+3 格式(如 EURUSD, GBPUSD) base = market_id[:3] quote = market_id[3:] symbol = base + '/' + quote else: - # 如果无法解析,使用原始 market_id symbol = market_id timestamp = self.parse8601(self.safe_string(order, 'openTime')) @@ -634,7 +611,7 @@ class mt5(Exchange, ImplicitAPI): 'price': price, 'stopLossPrice': self.safe_number(order, 'stopLoss'), 'takeProfitPrice': self.safe_number(order, 'takeProfit'), - 'reduceOnly':None, + 'reduceOnly': None, 'triggerPrice': None, 'amount': amount, 'filled': filled, @@ -686,7 +663,6 @@ class mt5(Exchange, ImplicitAPI): def parse_position(self, order_data, market: Market = None): """从订单数据解析持仓""" - # 只有状态为 Filled 的订单才是持仓 state = self.safe_string(order_data, 'state') if state != 'Filled': return None @@ -702,17 +678,14 @@ class mt5(Exchange, ImplicitAPI): if open_timestamp_utc: timestamp = open_timestamp_utc - # 确定持仓方向 order_type = self.safe_string(order_data, 'orderType') side = 'long' if order_type == 'Buy' else 'short' - # 计算持仓价值 contracts = self.safe_number(order_data, 'lots', 0) entry_price = self.safe_number(order_data, 'openPrice', 0) current_price = self.safe_number(order_data, 'closePrice', entry_price) notional = contracts * entry_price if contracts and entry_price else None - # 计算盈亏百分比 percentage = None if entry_price and current_price and entry_price != 0: if side == 'long': @@ -729,17 +702,17 @@ class mt5(Exchange, ImplicitAPI): 'contracts': contracts, 'contractSize': self.safe_number(order_data, 'contractSize', 1.0), 'entryPrice': entry_price, - 'markPrice': current_price, # 使用当前价格作为标记价格 + 'markPrice': current_price, 'notional': notional, - 'leverage': 1, # MT5 可能需要从账户信息获取 + 'leverage': 1, 'unrealizedPnl': self.safe_number(order_data, 'profit', 0), - 'realizedPnl': 0, # 对于持仓,已实现盈亏为0 - 'liquidationPrice': None, # MT5 可能不提供 + 'realizedPnl': 0, + 'liquidationPrice': None, 'marginMode': 'cross', 'percentage': percentage, 'marginRatio': None, 'collateral': None, - 'initialMargin': None, # 可能需要计算 + 'initialMargin': None, 'initialMarginPercentage': None, 'maintenanceMargin': None, 'maintenanceMarginPercentage': None, @@ -757,7 +730,6 @@ class mt5(Exchange, ImplicitAPI): 'volume': amount, } - # 映射订单类型 operation_map = { 'market': { 'buy': 'Buy', @@ -781,7 +753,6 @@ class mt5(Exchange, ImplicitAPI): if type in ['limit', 'stop'] and price is not None: request['price'] = price - # 处理止损止盈 stop_loss = self.safe_number(params, 'stopLoss') take_profit = self.safe_number(params, 'takeProfit') if stop_loss is not None: diff --git a/ccxt/pro/mt5.py b/ccxt/pro/mt5.py index 9b229d9..a7b4ce9 100644 --- a/ccxt/pro/mt5.py +++ b/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}") \ No newline at end of file diff --git a/test/atest_mt5_quick.py b/test/atest_mt5_quick.py index 3d6bb21..c851870 100644 --- a/test/atest_mt5_quick.py +++ b/test/atest_mt5_quick.py @@ -38,19 +38,20 @@ def quick_sync_test(): logger.info(f" ✅ 交易对数量: {len(markets)}") # # 测试余额 - # logger.info("2. 测试余额...") - # balance = exchange.fetch_balance() - # logger.info(f" ✅ 余额: {balance['total'].get('USDT', 'N/A')}") + logger.info("2. 测试余额...") + balance = exchange.fetch_balance() + logger.info(f" ✅ 余额: {balance['total'].get('USDT', 'N/A')}") # 测试行情 - # logger.info("3. 测试行情...") - # ticker = exchange.fetch_ticker('EUR/USD') - # logger.info(f" ✅ EUR/USD: {ticker['bid']} / {ticker['ask']}") + logger.info("3. 测试行情...") + ticker = exchange.fetch_ticker('BTC/USD') + logger.info(f" ✅ BTC/USD: {ticker['bid']} / {ticker['ask']}") # 测试订单 logger.info("3. 测试订单...") - ticker = exchange.fetch_closed_orders('EUR/USD') - logger.info(f" ✅ EUR/USD: {ticker['bid']} / {ticker['ask']}") + ticker = exchange.fetch_closed_orders('BTC/USD',limit=1) + # logger.info(f" ✅ BTC/USD: {ticker['bid']} / {ticker['ask']}") + logger.info(f" ✅ 订单数量: {len(ticker)}") logger.info("🎉 快速同步测试完成!") @@ -116,21 +117,21 @@ async def main(): if __name__ == "__main__": - # asyncio.run(main()) + asyncio.run(main()) - exchange = mt5_sync(CONFIG) + # exchange = mt5_sync(CONFIG) # 测试连接 - logger.info("1. 测试连接...") - token = exchange.get_token() - logger.info(f" ✅ Token: {token}") + # logger.info("1. 测试连接...") + # token = exchange.get_token() + # logger.info(f" ✅ Token: {token}") - # # 测试交易对 - logger.info("4. 测试交易对...") - markets = exchange.fetch_markets() - logger.info(f" ✅ 交易对数量: {len(markets)}") + # # # 测试交易对 + # logger.info("4. 测试交易对...") + # markets = exchange.fetch_markets() + # logger.info(f" ✅ 交易对数量: {len(markets)}") - logger.info("3. 测试订单...") - ticker = exchange.fetch_closed_orders('BTC/USD') - logger.info(f"{ticker}") \ No newline at end of file + # logger.info("3. 测试订单...") + # ticker = exchange.fetch_closed_orders('BTC/USD') + # logger.info(f"{ticker}") \ No newline at end of file