# -*- 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 import hashlib from ccxt.base.types import Any, Balances, DepositAddress, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, OrderBooks, Trade, TradingFees, Transaction from typing import List from typing import List, Optional # 添加 Optional 导入 from ccxt.base.errors import ExchangeError from ccxt.base.errors import AuthenticationError from ccxt.base.errors import ArgumentsRequired from ccxt.base.errors import InsufficientFunds from ccxt.base.errors import InvalidOrder from ccxt.base.errors import OrderNotFound from ccxt.base.errors import DDoSProtection from ccxt.base.errors import RateLimitExceeded from ccxt.base.errors import ExchangeNotAvailable from ccxt.base.errors import InvalidNonce from ccxt.base.decimal_to_precision import TICK_SIZE from ccxt.base.precise import Precise class mt5(Exchange, ImplicitAPI): def describe(self) -> Any: return self.deep_extend(super(mt5, self).describe(), { 'id': 'mt5', 'name': 'MT5', 'countries': ['US'], 'version': 'v2025.02.05-05.23', 'rateLimit': 1000, 'hostname': '43.167.188.220:5000', 'pro': True, 'options': { 'host': '18.163.85.196', 'port': 443, 'connectTimeoutSeconds': 30, }, 'has': { 'CORS': True, 'spot': True, 'margin': True, 'swap': False, 'future': False, 'option': False, 'cancelOrder': True, 'createOrder': True, 'fetchBalance': True, 'fetchClosedOrders': True, 'fetchMarkets': True, 'fetchMyTrades': True, 'fetchOHLCV': True, 'fetchOpenOrders': True, 'fetchOrder': True, 'fetchOrderBook': True, 'fetchTicker': True, 'fetchTickers': True, # 添加这个字段 'fetchTickers': True, }, 'timeframes': { '1m': 1, '5m': 5, '15m': 15, '30m': 30, '1h': 60, '4h': 240, '1d': 1440, '1w': 10080, '1M': 43200, }, 'urls': { 'logo': '', 'api': { 'public': 'http://43.167.188.220:5000', # 直接使用具体地址 'private': 'http://43.167.188.220:5000', }, 'www': 'http://43.167.188.220:5000', 'doc': ['http://43.167.188.220:5000/index.html'], }, 'api': { 'public': { 'get': { 'Ping': 1 }, }, 'private': { 'get': { 'Connect': 10, 'CheckConnect': 1, 'Disconnect': 1, 'Symbols': 1, 'ServerTimezone':1, 'AccountSummary': 1, 'AccountDetails': 1, 'SymbolList': 1, 'GetQuote': 1, 'GetQuoteMany': 1, 'MarketWatchMany': 1, 'OpenedOrders': 1, 'ClosedOrders': 1, 'OpenedOrder': 1, 'OrderHistory': 1, 'PriceHistory': 1, 'OrderSend': 1, 'OrderModify': 1, 'OrderClose': 1, }, }, }, 'requiredCredentials': { 'apiKey': True, 'secret': True, 'hostname': True, }, 'commonCurrencies': {}, 'exceptions': { 'exact': { 'Invalid token': AuthenticationError, 'Connection failed': ExchangeError, 'Invalid symbol': ExchangeError, 'Invalid order': InvalidOrder, 'Order not found': OrderNotFound, }, }, }) async def get_token(self): """获取或刷新 token - 异步版本""" if hasattr(self, 'token') and self.token: try: await self.check_connect() return self.token except Exception: # Token 无效,重新连接 pass # 重新连接获取 token return await self.connect() async def connect(self): """连接到 MT5 账户并获取 token - 异步版本""" request = { 'user': self.apiKey, 'password': self.secret, 'host': self.options['host'], 'port': self.options['port'], 'connectTimeoutSeconds': self.options['connectTimeoutSeconds'], } response = await self.private_get_connect(request) self.token = response return self.token async def check_connect(self): """检查连接状态 - 异步版本""" request = { 'id': await self.get_token(), } return await self.private_get_checkconnect(request) async def fetch_markets(self, params={}): """获取交易对列表 - 异步修复版本""" if not hasattr(self, 'token') or not self.token: await self.get_token() request = { 'id': self.token, } try: response = await self.private_get_symbols(self.extend(request, params)) markets = [] if isinstance(response, dict): for symbol, info in response.items(): try: market = self.parse_market(info) if market and market.get('symbol'): markets.append(market) except Exception as e: # 跳过解析失败的市场,继续处理其他市场 if self.verbose: print(f"跳过交易对 {symbol}: {e}") continue # 设置市场数据 if markets: self.markets = {} self.symbols = [] for market in markets: id = market['id'] symbol = market['symbol'] self.markets[id] = market self.markets[symbol] = market self.symbols.append(symbol) self.symbols = sorted(self.symbols) self.ids = sorted(self.markets.keys()) return markets except Exception as e: raise ExchangeError(f"获取市场数据失败: {e}") def parse_market(self, info): """解析市场信息 - 更健壮的版本""" try: # 安全获取 symbol if not isinstance(info, dict): return None symbol = self.safe_string(info, 'currency', '') if not symbol: return None symbol = symbol.upper().strip() # 确保符号格式正确 (如 EURUSD) if len(symbol) < 6: return None base = symbol[:3] quote = symbol[3:] # 安全处理精度 digits = self.safe_integer(info, 'digits', 5) # 确保 digits 是整数 if digits is not None: try: digits = int(digits) except (ValueError, TypeError): digits = 5 market_id = symbol return { 'id': market_id, 'symbol': base + '/' + quote, 'base': base, 'quote': quote, 'baseId': base, 'quoteId': quote, 'active': True, 'type': 'spot', 'spot': True, 'margin': True, 'precision': { 'price': digits, 'amount': 2, }, 'limits': { 'amount': { 'min': self.safe_number(info, 'minVolume', 0.01), 'max': self.safe_number(info, 'maxVolume'), }, 'price': { 'min': None, 'max': None, }, 'cost': { 'min': None, 'max': None, }, }, 'info': info, } except Exception as e: if self.verbose: print(f"解析市场信息失败: {e}, info: {info}") return None async def fetch_balance(self, params={}): """获取账户余额""" if not hasattr(self, 'token') or not self.token: await self.get_token() request = { 'id': self.token, } response = await self.private_get_accountsummary(self.extend(request, params)) return self.parse_balance(response) def parse_balance(self, response): """解析余额信息""" result = { 'info': response, 'timestamp': None, 'datetime': None, } 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) result[currency] = { 'free': free_margin, 'used': margin, 'total': balance, } return self.safe_balance(result) async def fetch_ticker(self, symbol, 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'], } response = await self.private_get_getquote(self.extend(request, params)) return self.parse_ticker(response, market) def parse_ticker(self, ticker, market=None): """解析行情数据""" symbol = market['symbol'] if market else None timestamp = None if ticker.get('time'): try: timestamp = self.parse8601(ticker.get('time')) except: timestamp = None return { 'symbol': symbol, 'timestamp': timestamp, 'datetime': self.iso8601(timestamp) if timestamp else None, 'high': None, 'low': None, 'bid': self.safe_number(ticker, 'bid'), 'bidVolume': None, 'ask': self.safe_number(ticker, 'ask'), 'askVolume': None, 'vwap': None, 'open': None, 'close': None, 'last': self.safe_number(ticker, 'last'), 'previousClose': None, 'change': None, 'percentage': None, 'average': None, 'baseVolume': self.safe_number(ticker, 'volume'), 'quoteVolume': None, 'info': ticker, } async def fetch_tickers(self, symbols: Optional[List[str]] = None, params={}): """异步获取多个交易对的行情数据""" if not hasattr(self, 'token') or not self.token: await self.get_token() request = { 'id': self.token, } # 如果指定了特定的交易对 if symbols is not None: # 将符号列表转换为 MT5 格式 mt5_symbols = [] for symbol in symbols: market = self.market(symbol) mt5_symbols.append(market['id']) request['symbols'] = mt5_symbols try: response = await self.private_get_getquotemany(self.extend(request, params)) return self.parse_tickers(response, symbols) except Exception as e: # 如果批量获取失败,回退到逐个获取 if symbols is not None: return await self.fetch_tickers_fallback(symbols, params) else: raise ExchangeError(f"获取批量行情失败: {e}") async def fetch_tickers_fallback(self, symbols, params={}): """异步回退方法:逐个获取交易对行情""" tickers = {} for symbol in symbols: try: ticker = await self.fetch_ticker(symbol, params) tickers[symbol] = ticker except Exception as e: if self.verbose: print(f"获取 {symbol} 行情失败: {e}") continue return tickers def parse_tickers(self, response, symbols=None): """解析批量行情数据(与同步版本相同)""" tickers = {} if isinstance(response, list): # 如果响应是数组 for ticker_data in response: try: ticker = self.parse_ticker(ticker_data) if ticker and ticker.get('symbol'): tickers[ticker['symbol']] = ticker except Exception as e: if self.verbose: print(f"解析行情数据失败: {e}") continue elif isinstance(response, dict): # 如果响应是字典 for symbol_key, ticker_data in response.items(): try: ticker = self.parse_ticker(ticker_data) if ticker and ticker.get('symbol'): tickers[ticker['symbol']] = ticker except Exception as e: if self.verbose: print(f"解析行情数据失败 {symbol_key}: {e}") continue # 如果指定了特定的交易对,确保返回的顺序一致 if symbols is not None: ordered_tickers = {} for symbol in symbols: if symbol in tickers: ordered_tickers[symbol] = tickers[symbol] return ordered_tickers return tickers async def fetch_open_orders(self, symbol=None, since=None, limit=None, params={}): """异步获取未平仓订单 - 修复版本""" if not hasattr(self, 'token') or not self.token: await self.get_token() request = { 'id': self.token, } response = await self.private_get_openedorders(self.extend(request, params)) # 如果指定了特定交易对,进行过滤 if symbol is not None: market = self.market(symbol) filtered_orders = [] for order in response: if isinstance(order, dict) and order.get('symbol') == market['id']: filtered_orders.append(order) return self.parse_orders(filtered_orders, market, since, limit) else: return self.parse_orders(response, None, since, limit) async def fetch_closed_orders(self, symbol=None, since=None, limit=None, params={}): """异步获取已平仓订单 - 修复版本""" if not hasattr(self, 'token') or not self.token: await self.get_token() request = { 'id': self.token, } response = await self.private_get_closedorders(self.extend(request, params)) # 如果指定了特定交易对,进行过滤 if symbol is not None: market = self.market(symbol) filtered_orders = [] for order in response: if isinstance(order, dict) and order.get('symbol') == market['id']: filtered_orders.append(order) return self.parse_orders(filtered_orders, market, since, limit) else: 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')) last_trade_timestamp = self.parse8601(self.safe_string(order, 'closeTime')) status = self.parse_order_status(self.safe_string(order, 'state')) side = self.parse_order_side(self.safe_string(order, 'orderType')) type = self.parse_order_type(self.safe_string(order, 'orderType')) price = self.safe_number(order, 'openPrice') amount = self.safe_number(order, 'lots') filled = self.safe_number(order, 'closeLots', 0) remaining = None if amount is not None and filled is not None: remaining = amount - filled cost = None if price is not None and filled is not None: cost = price * filled fee = None fee_cost = self.safe_number(order, 'commission', 0) if fee_cost != 0: fee = { 'cost': fee_cost, 'currency': None, } return self.safe_order({ 'id': id, 'clientOrderId': None, 'datetime': self.iso8601(timestamp), 'timestamp': timestamp, 'lastTradeTimestamp': last_trade_timestamp, 'status': status, 'symbol': symbol, 'type': type, 'timeInForce': None, 'postOnly': None, 'side': side, 'price': price, 'stopPrice': None, 'triggerPrice': None, 'amount': amount, 'filled': filled, 'remaining': remaining, 'cost': cost, 'trades': None, 'fee': fee, 'info': order, 'average': None, }) except Exception as e: if self.verbose: print(f"解析订单失败: {e}, order: {order}") raise e def parse_order_status(self, status): statuses = { 'Started': 'open', 'Placed': 'open', 'Cancelled': 'canceled', 'Partial': 'open', 'Filled': 'closed', 'Rejected': 'rejected', 'Expired': 'expired', } return self.safe_string(statuses, status, status) def parse_order_side(self, side): sides = { 'Buy': 'buy', 'Sell': 'sell', 'BuyLimit': 'buy', 'SellLimit': 'sell', 'BuyStop': 'buy', 'SellStop': 'sell', } return self.safe_string(sides, side, side) def parse_order_type(self, type): types = { 'Buy': 'market', 'Sell': 'market', 'BuyLimit': 'limit', 'SellLimit': 'limit', 'BuyStop': 'stop', 'SellStop': 'stop', } return self.safe_string(types, type, type) async def create_order(self, symbol, type, side, amount, price=None, 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'], 'volume': amount, } # 映射订单类型 operation_map = { 'market': { 'buy': 'Buy', 'sell': 'Sell', }, 'limit': { 'buy': 'BuyLimit', 'sell': 'SellLimit', }, 'stop': { 'buy': 'BuyStop', 'sell': 'SellStop', }, } if type in operation_map and side in operation_map[type]: request['operation'] = operation_map[type][side] else: raise InvalidOrder(self.id + ' createOrder does not support order type ' + type + ' and side ' + side) 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: request['stoploss'] = stop_loss if take_profit is not None: request['takeprofit'] = take_profit response = await self.private_get_ordersend(self.extend(request, params)) return self.parse_order(response, market) async def cancel_order(self, id, symbol=None, params={}): """取消订单""" if not hasattr(self, 'token') or not self.token: await self.get_token() request = { 'id': self.token, 'ticket': int(id), } response = await self.private_get_orderclose(self.extend(request, params)) return self.parse_order(response) def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): """签名请求""" base_url = self.urls['api'][api] url = base_url + '/' + path query = self.omit(params, self.extract_params(path)) if method == 'GET' and query: url += '?' + self.urlencode(query) # 调试信息 if self.verbose: print(f"🔧 Debug: Final URL: {url}") print(f"🔧 Debug: Method: {method}") print(f"🔧 Debug: Params: {params}") return { 'url': url, 'method': method, 'body': body, 'headers': headers }