This commit is contained in:
cgit
2025-11-22 18:52:07 +08:00
parent 8646036ca5
commit 82c081a8e7
3 changed files with 2 additions and 628 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
build/
dist/

View File

@@ -1,316 +0,0 @@
# -*- 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 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,
},
'has': {
'CORS': True,
'spot': True,
'margin': True,
'swap': True,
'future': True,
'option': True,
'borrowCrossMargin': True,
'cancelAllOrders': True,
'cancelAllOrdersAfter': True,
'cancelOrder': True,
'cancelOrders': True,
'cancelOrdersForSymbols': True,
'closeAllPositions': False,
'closePosition': False,
'createConvertTrade': True,
'createMarketBuyOrderWithCost': True,
'createMarketSellOrderWithCost': True,
'createOrder': True,
'createOrders': True,
'createOrderWithTakeProfitAndStopLoss': True,
'createPostOnlyOrder': True,
'createReduceOnlyOrder': True,
'createStopLimitOrder': True,
'createStopLossOrder': True,
'createStopMarketOrder': True,
'createStopOrder': True,
'createTakeProfitOrder': True,
'createTrailingAmountOrder': True,
'createTriggerOrder': True,
'editOrder': True,
'editOrders': True,
'fetchAllGreeks': True,
'fetchBalance': True,
'fetchBidsAsks': 'emulated',
'fetchBorrowInterest': False, # temporarily disabled, doesn't work
'fetchBorrowRateHistories': False,
'fetchBorrowRateHistory': False,
'fetchCanceledAndClosedOrders': True,
'fetchCanceledOrders': True,
'fetchClosedOrder': True,
'fetchClosedOrders': True,
'fetchConvertCurrencies': True,
'fetchConvertQuote': True,
'fetchConvertTrade': True,
'fetchConvertTradeHistory': True,
'fetchCrossBorrowRate': True,
'fetchCrossBorrowRates': False,
'fetchCurrencies': True,
'fetchDeposit': False,
'fetchDepositAddress': True,
'fetchDepositAddresses': False,
'fetchDepositAddressesByNetwork': True,
'fetchDeposits': True,
'fetchDepositWithdrawFee': 'emulated',
'fetchDepositWithdrawFees': True,
'fetchFundingHistory': True,
'fetchFundingRate': 'emulated', # emulated in exchange
'fetchFundingRateHistory': True,
'fetchFundingRates': True,
'fetchGreeks': True,
'fetchIndexOHLCV': True,
'fetchIsolatedBorrowRate': False,
'fetchIsolatedBorrowRates': False,
'fetchLedger': True,
'fetchLeverage': True,
'fetchLeverageTiers': True,
'fetchLongShortRatio': False,
'fetchLongShortRatioHistory': True,
'fetchMarginAdjustmentHistory': False,
'fetchMarketLeverageTiers': True,
'fetchMarkets': True,
'fetchMarkOHLCV': True,
'fetchMyLiquidations': True,
'fetchMySettlementHistory': True,
'fetchMyTrades': True,
'fetchOHLCV': True,
'fetchOpenInterest': True,
'fetchOpenInterestHistory': True,
'fetchOpenOrder': True,
'fetchOpenOrders': True,
'fetchOption': True,
'fetchOptionChain': True,
'fetchOrder': True,
'fetchOrderBook': True,
'fetchOrders': False,
'fetchOrderTrades': True,
'fetchPosition': True,
'fetchPositionHistory': 'emulated',
'fetchPositions': True,
'fetchPositionsHistory': True,
'fetchPremiumIndexOHLCV': True,
'fetchSettlementHistory': True,
'fetchTicker': True,
'fetchTickers': True,
'fetchTime': True,
'fetchTrades': True,
'fetchTradingFee': True,
'fetchTradingFees': True,
'fetchTransactions': False,
'fetchTransfers': True,
'fetchUnderlyingAssets': False,
'fetchVolatilityHistory': True,
'fetchWithdrawals': True,
'repayCrossMargin': True,
'sandbox': True,
'setLeverage': True,
'setMarginMode': True,
'setPositionMode': True,
'transfer': True,
'withdraw': 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://{hostname}',
'private': 'http://{hostname}',
},
'www': 'http://{hostname}',
'doc': ['http://{hostname}/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,
},
},
'wsEndpoint': {
'order': "OnOrderUpdate",
'quote': "OnQuote",
'orderbook': "OnOrderBook",
}
},
'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 self.token and self.token_checked:
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': 30,
}
print(f"🔧 Debug: Connect request params: {request}")
response = await self.private_get_connect(request)
print(f"🔧 Debug: Connect response: {response}")
self.token = response
self.token_checked = True
return self.token
async def check_connect(self):
"""检查连接状态 - 异步版本"""
request = {
'id': await self.get_token(),
}
return await self.private_get_check_connect(request)
async def fetch_markets(self, params={}):
"""获取交易对列表"""
if not self.token:
await self.get_token()
request = {
'id': self.token,
}
response = await self.private_get_symbols(self.deep_extend(request, params))
markets = []
if isinstance(response, dict):
for symbol, info in response.items():
market = self.parse_market(info)
if market:
markets.append(market)
return markets
def parse_market(self, info):
"""解析市场信息 - 根据 SymbolInfo 结构修正"""
symbol = info.get('currency', '').upper()
if not symbol:
return None
return {
'id': symbol,
'symbol': symbol,
'base': symbol[:3] if len(symbol) >= 6 else symbol,
'quote': symbol[3:] if len(symbol) >= 6 else 'USD',
'active': True,
'precision': {
'price': info.get('digits', 5),
'amount': 2,
},
'limits': {
'amount': {
'min': 0.01,
'max': None,
},
'price': {
'min': None,
'max': None,
},
'cost': {
'min': None,
'max': None,
},
},
'info': info,
}

View File

@@ -1,312 +0,0 @@
# -*- 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
}