874 lines
31 KiB
Python
874 lines
31 KiB
Python
# -*- coding: utf-8 -*-
|
||
|
||
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,
|
||
},
|
||
'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,
|
||
'SubscribeOhlc': 1, # K线订阅
|
||
'UnsubscribeOhlc': 1, # K线取消订阅
|
||
'Subscribe': 1, # 行情订阅
|
||
'UnSubscribe': 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 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:
|
||
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:
|
||
# 处理较短的符号
|
||
base = symbol
|
||
quote = 'USD' # 默认报价货币
|
||
else:
|
||
base = symbol[:3]
|
||
quote = symbol[3:]
|
||
|
||
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 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:
|
||
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)
|
||
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()
|
||
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,
|
||
}
|
||
# 确保所有必需字段都有值
|
||
if timestamp is None:
|
||
timestamp = self.milliseconds()
|
||
|
||
return self.safe_order({
|
||
'id': id,
|
||
'clientOrderId': None,
|
||
'datetime': self.iso8601(timestamp),
|
||
'timestamp': timestamp,
|
||
'lastTradeTimestamp': last_trade_timestamp,
|
||
'lastUpdateTimestamp': last_trade_timestamp,
|
||
'status': status,
|
||
'symbol': symbol,
|
||
'type': type,
|
||
'timeInForce': None,
|
||
'postOnly': None,
|
||
'side': side,
|
||
'price': price,
|
||
'stopLossPrice': self.safe_number(order, 'stopLoss'),
|
||
'takeProfitPrice': self.safe_number(order, 'takeProfit'),
|
||
'reduceOnly':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}")
|
||
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 = {
|
||
'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)
|
||
|
||
def parse_position(self, order_data, market: Market = None):
|
||
"""从订单数据解析持仓"""
|
||
# 只有状态为 Filled 的订单才是持仓
|
||
state = self.safe_string(order_data, 'state')
|
||
if state != 'Filled':
|
||
return None
|
||
|
||
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'))
|
||
open_timestamp_utc = self.safe_integer(order_data, 'openTimestampUTC')
|
||
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':
|
||
percentage = (current_price - entry_price) / entry_price
|
||
else:
|
||
percentage = (entry_price - current_price) / entry_price
|
||
|
||
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': current_price, # 使用当前价格作为标记价格
|
||
'notional': notional,
|
||
'leverage': 1, # MT5 可能需要从账户信息获取
|
||
'unrealizedPnl': self.safe_number(order_data, 'profit', 0),
|
||
'realizedPnl': 0, # 对于持仓,已实现盈亏为0
|
||
'liquidationPrice': None, # MT5 可能不提供
|
||
'marginMode': 'cross',
|
||
'percentage': percentage,
|
||
'marginRatio': None,
|
||
'collateral': None,
|
||
'initialMargin': None, # 可能需要计算
|
||
'initialMarginPercentage': None,
|
||
'maintenanceMargin': None,
|
||
'maintenanceMarginPercentage': None,
|
||
# 'info': order_data,
|
||
}
|
||
|
||
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)
|
||
|
||
async def private_get(self, endpoint, params={}):
|
||
"""发送私有 GET 请求"""
|
||
return await self.fetch_private(endpoint, 'GET', params)
|
||
|
||
async def fetch_private(self, path, method='GET', params={}, headers=None, body=None):
|
||
"""发送私有 API 请求"""
|
||
url = self.urls['api']['private'] + '/' + path
|
||
query = self.omit(params, self.extract_params(path))
|
||
|
||
if method == 'GET' and query:
|
||
url += '?' + self.urlencode(query)
|
||
|
||
if self.verbose:
|
||
print(f"🔧 发送请求: {url}")
|
||
|
||
import aiohttp
|
||
try:
|
||
async with aiohttp.ClientSession() as session:
|
||
async with session.get(url, timeout=aiohttp.ClientTimeout(total=30)) as response:
|
||
if response.status == 200:
|
||
content = await response.text()
|
||
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}")
|
||
|
||
def parse_json(self, response):
|
||
"""解析响应,支持多种格式"""
|
||
if not response:
|
||
return response
|
||
|
||
response = response.strip()
|
||
|
||
# 处理常见的成功响应
|
||
if response in ['OK', 'SUCCESS', 'True', 'true']:
|
||
return True
|
||
|
||
# 处理常见的失败响应
|
||
if response in ['FAIL', 'ERROR', 'False', 'false']:
|
||
return False
|
||
|
||
# 尝试解析 JSON
|
||
try:
|
||
import json
|
||
return json.loads(response)
|
||
except json.JSONDecodeError:
|
||
# 不是 JSON,返回原始响应
|
||
return response
|
||
except Exception as e:
|
||
if self.verbose:
|
||
print(f"响应解析失败: {e}, 响应: {response}")
|
||
return 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
|
||
} |