313 lines
11 KiB
Python
313 lines
11 KiB
Python
# -*- 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
|
||
|
||
|
||
class mt5(mt5Parent):
|
||
def describe(self):
|
||
return self.deep_extend(super(mt5, self).describe(), {
|
||
'platinum': True,
|
||
'hostname': '43.167.188.220:5000',
|
||
'has': {
|
||
# 专业版特有功能
|
||
'watchPosition': True,
|
||
'watchOrder': True,
|
||
'watchLeverage': True,
|
||
'watchMargin': True,
|
||
'advancedAPI': True,
|
||
'batchOrders': True,
|
||
'modifyOrder': True,
|
||
'closePosition': True,
|
||
'setLeverage': True,
|
||
'setMargin': True,
|
||
},
|
||
'urls': {
|
||
'api': {
|
||
'ws': 'ws://{hostname}',
|
||
},
|
||
},
|
||
'options': {
|
||
'defaultType': 'spot',
|
||
'watchOrder': {
|
||
'symbol': None,
|
||
'orderId': None,
|
||
},
|
||
'watchPosition': {
|
||
'symbol': None,
|
||
},
|
||
},
|
||
})
|
||
|
||
|
||
|
||
|
||
# 专业版特有方法
|
||
async def watch_order(self, params={}):
|
||
"""
|
||
监听特定订单的更新(专业版特有)
|
||
"""
|
||
|
||
if not self.token:
|
||
await self.get_token()
|
||
|
||
try:
|
||
await self.load_markets()
|
||
except Exception as e:
|
||
print(f"错误: {e}")
|
||
|
||
# await self.fetch_markets()
|
||
|
||
# return
|
||
request: dict = {
|
||
'id': self.token,
|
||
}
|
||
endpoint = self.api['wsEndpoint']['order']
|
||
url = self.implode_hostname(self.urls['api']['ws']) + '/' + endpoint
|
||
url += '?' + self.urlencode(request)
|
||
return await self.watch(url,'order_position')
|
||
|
||
def handle_message(self, client: Client, message):
|
||
# print(message)
|
||
errorCode = self.safe_string(message, 'errorCode')
|
||
if errorCode is not None:
|
||
self.handle_error_message(client, message)
|
||
return
|
||
|
||
# 处理 MT5 特定的消息
|
||
message_type = self.safe_string(message, 'type')
|
||
|
||
|
||
if message_type == 'OpenedOrders':
|
||
print("111111111111111")
|
||
self.handle_opened_orders(client, message)
|
||
elif message_type == self.api['wsEndpoint']['orderbook']:
|
||
self.handle_orderbook(client, message)
|
||
elif message_type == self.api['wsEndpoint']['quote']:
|
||
self.handle_ticker(client, message)
|
||
|
||
def handle_opened_orders(self, client, message):
|
||
"""
|
||
处理 OpenedOrders 消息
|
||
"""
|
||
orders_data = self.safe_value(message, 'data', [])
|
||
message_id = self.safe_string(message, 'id')
|
||
timestamp = self.safe_integer(message, 'timestampUTC')
|
||
parsed_orders = []
|
||
for order_data in orders_data:
|
||
# 解析每个订单
|
||
order = self.parse_ws_order(order_data)
|
||
parsed_orders.append(order)
|
||
|
||
# 分发到不同的消息流
|
||
order_id = order['id']
|
||
symbol = order['symbol']
|
||
|
||
# 更新特定订单
|
||
client.resolve(order, 'order:' + order_id)
|
||
|
||
# 更新符号特定的订单列表
|
||
client.resolve([order], 'orders:' + symbol)
|
||
|
||
print('-------------parsed_orders:',parsed_orders)
|
||
# 更新所有订单列表
|
||
client.resolve(parsed_orders, 'orders')
|
||
|
||
# 如果有请求ID,也解析到该请求
|
||
if message_id:
|
||
client.resolve(parsed_orders, 'request:' + message_id)
|
||
|
||
def handle_orderbook(self, client, message):
|
||
"""处理深度数据"""
|
||
orderbook = self.parse_ws_order_book(message)
|
||
symbol = orderbook['symbol']
|
||
message_hash = 'orderbook:' + symbol
|
||
client.resolve(orderbook, message_hash)
|
||
|
||
def parse_ws_order(self, order, market=None):
|
||
"""
|
||
解析 MT5 WebSocket 订单数据为 CCXT 标准格式
|
||
"""
|
||
# 提取订单基本信息
|
||
ticket = self.safe_integer(order, 'ticket')
|
||
symbol = self.safe_string(order, 'symbol')
|
||
order_type = self.safe_string(order, 'orderType')
|
||
deal_type = self.safe_string(order, 'dealType')
|
||
state = self.safe_string(order, 'state')
|
||
lots = self.safe_number(order, 'lots')
|
||
contract_size = self.safe_number(order, 'contractSize', 1.0)
|
||
open_price = self.safe_number(order, 'openPrice')
|
||
close_price = self.safe_number(order, 'closePrice')
|
||
profit = self.safe_number(order, 'profit')
|
||
print("ggeggegge========")
|
||
# 获取市场信息
|
||
market = self.market(symbol) if market is None else market
|
||
|
||
# 解析时间戳
|
||
open_time_str = self.safe_string(order, 'openTime')
|
||
open_timestamp = self.safe_integer(order, 'openTimestampUTC')
|
||
|
||
# 如果 openTimestampUTC 不存在,尝试解析 openTime 字符串
|
||
if open_timestamp is None and open_time_str:
|
||
try:
|
||
# 解析格式: "2025-11-15T04:06:06.994"
|
||
open_timestamp = self.parse8601(open_time_str)
|
||
except:
|
||
open_timestamp = None
|
||
|
||
# 解析订单状态
|
||
status = self.parse_order_status(state)
|
||
|
||
# 解析订单方向
|
||
side = self.parse_order_side(order_type, deal_type)
|
||
|
||
# 解析订单类型
|
||
order_type_parsed = self.parse_order_type(order_type)
|
||
|
||
# 计算数量 (lots * contractSize)
|
||
amount = lots * contract_size if (lots is not None and contract_size is not None) else None
|
||
|
||
# 解析成交数量
|
||
volume = self.safe_integer(order, 'volume', 0)
|
||
close_volume = self.safe_integer(order, 'closeVolume', 0)
|
||
|
||
# 对于已成交订单,filled 应该是 volume
|
||
filled = volume
|
||
remaining = 0 # MT5 中订单要么完全成交,要么没有
|
||
|
||
# 解析止盈止损
|
||
stop_loss = self.safe_number(order, 'stopLoss')
|
||
take_profit = self.safe_number(order, 'takeProfit')
|
||
|
||
# 构建标准订单对象
|
||
result = {
|
||
'id': str(ticket),
|
||
'clientOrderId': None,
|
||
'datetime': self.iso8601(open_timestamp) if open_timestamp else None,
|
||
'timestamp': open_timestamp,
|
||
'lastTradeTimestamp': None,
|
||
'status': status,
|
||
'symbol': market['symbol'] if market else symbol,
|
||
'type': order_type_parsed,
|
||
'side': side,
|
||
'price': open_price,
|
||
'amount': amount,
|
||
'filled': filled,
|
||
'remaining': remaining,
|
||
'cost': None, # 可以计算: amount * price
|
||
'average': None,
|
||
'fee': {
|
||
'currency': market['quote'] if market else 'USD',
|
||
'cost': self.safe_number(order, 'fee', 0),
|
||
},
|
||
'trades': None,
|
||
'info': order,
|
||
}
|
||
|
||
# 计算成本
|
||
if amount is not None and open_price is not None:
|
||
result['cost'] = amount * open_price
|
||
|
||
# 添加 MT5 特定字段
|
||
result['stopLoss'] = stop_loss
|
||
result['takeProfit'] = take_profit
|
||
result['profit'] = profit
|
||
result['commission'] = self.safe_number(order, 'commission', 0)
|
||
result['swap'] = self.safe_number(order, 'swap', 0)
|
||
result['comment'] = self.safe_string(order, 'comment', '')
|
||
print("-----------result:",result)
|
||
return result
|
||
|
||
def parse_order_status(self, status):
|
||
"""
|
||
解析 MT5 订单状态
|
||
"""
|
||
statuses = {
|
||
'Filled': 'closed', # 已成交
|
||
'PartialFilled': 'open', # 部分成交
|
||
'Pending': 'open', # 挂单中
|
||
'Cancelled': 'canceled', # 已取消
|
||
'Rejected': 'rejected', # 已拒绝
|
||
'Expired': 'expired', # 已过期
|
||
# 根据您的数据添加更多状态映射
|
||
'Active': 'open',
|
||
'Closed': 'closed',
|
||
}
|
||
return self.safe_string(statuses, status, status.lower() if status else 'unknown')
|
||
|
||
def parse_order_side(self, order_type, deal_type):
|
||
"""
|
||
解析订单方向
|
||
"""
|
||
# 优先使用 deal_type,因为它更准确
|
||
if deal_type:
|
||
if 'Buy' in deal_type:
|
||
return 'buy'
|
||
elif 'Sell' in deal_type:
|
||
return 'sell'
|
||
|
||
# 其次使用 order_type
|
||
if order_type:
|
||
if order_type == 'Buy':
|
||
return 'buy'
|
||
elif order_type == 'Sell':
|
||
return 'sell'
|
||
elif 'Buy' in order_type:
|
||
return 'buy'
|
||
elif 'Sell' in order_type:
|
||
return 'sell'
|
||
|
||
return 'unknown'
|
||
|
||
def parse_order_type(self, order_type):
|
||
"""
|
||
解析订单类型
|
||
"""
|
||
if not order_type:
|
||
return 'market' # 默认类型
|
||
|
||
order_type_lower = order_type.lower()
|
||
|
||
if 'limit' in order_type_lower:
|
||
return 'limit'
|
||
elif 'stop' in order_type_lower:
|
||
return 'stop'
|
||
elif 'market' in order_type_lower:
|
||
return 'market'
|
||
else:
|
||
# 根据 dealType 判断
|
||
return 'market' # 默认为市价单
|
||
|
||
def sign(self, path, api='public', method='GET', params={}, headers=None, body=None):
|
||
"""签名请求 URL 构建"""
|
||
endpoint = '/' + self.implode_params(path, params)
|
||
url = self.implode_hostname(self.urls['api'][api]) + endpoint
|
||
headers = headers if (headers is not None) else {}
|
||
|
||
# 对于 GET 请求,将参数添加到查询字符串
|
||
if method == 'GET' and params:
|
||
# 特殊处理数组参数
|
||
query_params = {}
|
||
for key, value in params.items():
|
||
if isinstance(value, list):
|
||
# 对于数组参数,可能需要特殊编码
|
||
query_params[key] = ','.join(value)
|
||
else:
|
||
query_params[key] = value
|
||
|
||
url += '?' + self.urlencode(query_params)
|
||
elif method == 'GET' and params:
|
||
url += '?' + self.urlencode(params)
|
||
|
||
# print(f"🔧 Debug: Final URL: {url}")
|
||
|
||
return {
|
||
'url': url,
|
||
'method': method,
|
||
'body': body,
|
||
'headers': headers
|
||
}
|