Files
ccxt_with_mt5/ccxt/mt5.py
lz_db 42a0391eeb 1
2025-11-17 17:18:51 +08:00

818 lines
29 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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
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 PermissionDenied
from ccxt.base.errors import AccountSuspended
from ccxt.base.errors import ArgumentsRequired
from ccxt.base.errors import BadRequest
from ccxt.base.errors import BadSymbol
from ccxt.base.errors import NoChange
from ccxt.base.errors import MarginModeAlreadySet
from ccxt.base.errors import ManualInteractionNeeded
from ccxt.base.errors import InsufficientFunds
from ccxt.base.errors import InvalidOrder
from ccxt.base.errors import OrderNotFound
from ccxt.base.errors import NotSupported
from ccxt.base.errors import RateLimitExceeded
from ccxt.base.errors import InvalidNonce
from ccxt.base.errors import RequestTimeout
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', # 直接使用具体地址,不使用 {hostname}
'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, # 添加订阅端点
},
},
},
'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,
},
},
})
def ping_server(self):
"""测试连接"""
response = self.public_get_ping()
if response == 'OK':
return True
else:
return False
def get_token(self):
"""获取或刷新 token"""
if hasattr(self, 'token') and self.token:
try:
self.check_connect()
return self.token
except Exception:
# Token 无效,重新连接
pass
# 重新连接获取 token
return self.connect()
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服务器端口
'connectTimeoutSeconds': self.options['connectTimeoutSeconds'],
}
response = self.private_get_connect(params)
self.token = response
return self.token
def check_connect(self):
"""检查连接状态"""
params = {
'id': self.get_token(),
}
return self.private_get_checkconnect(params)
def disconnect(self):
"""断开连接"""
if hasattr(self, 'token') and self.token:
params = {
'id': self.token,
}
try:
self.private_get_disconnect(params)
except Exception:
pass
finally:
self.token = None
def load_token(self):
"""确保 token 已加载"""
if not hasattr(self, 'token') or not self.token:
self.get_token()
def server_timezone(self):
"""获得mt5服务器时区"""
if hasattr(self, 'timezone'):
return self.timezone
else:
self.load_token()
request = {
'id': self.token,
}
response = self.private_get_servertimezone(request)
self.timezone = int(float(response))
return self.timezone
def fetch_markets(self, params={}):
"""获取交易对列表 - 修复版本"""
self.load_token()
request = {
'id': self.token,
}
try:
response = 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, BTCUSD)
if len(symbol) < 6:
# 处理较短的符号(如黄金 XAUUSD 是6位但可能有其他情况
# 对于无法确定格式的符号,直接使用原始符号
base = symbol
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)
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
def fetch_ticker(self, symbol, params={}):
"""获取行情数据"""
self.load_token()
market = self.market(symbol)
request = {
'id': self.token,
'symbol': market['id'],
}
response = 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,
}
def fetch_tickers(self, symbols: Optional[List[str]] = None, params={}):
"""获取多个交易对的行情数据"""
self.load_token()
request = {
'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)
mt5_symbols.append(market['id'])
request['symbols'] = mt5_symbols
try:
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:
raise ExchangeError(f"获取批量行情失败: {e}")
def fetch_tickers_fallback(self, symbols, params={}):
"""回退方法:逐个获取交易对行情"""
tickers = {}
for symbol in symbols:
try:
ticker = 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
def fetch_order_book(self, symbol, limit=None, params={}):
"""获取订单簿"""
self.load_token()
market = self.market(symbol)
request = {
'id': self.token,
'symbol': market['id'],
}
response = self.private_get_getquote(self.extend(request, params))
# MT5 的 GetQuote 返回的是最新报价,不是完整的订单簿
# 这里模拟一个简单的订单簿
bid = self.safe_number(response, 'bid')
ask = self.safe_number(response, 'ask')
return {
'symbol': symbol,
'bids': [[bid, 1]] if bid else [],
'asks': [[ask, 1]] if ask else [],
'timestamp': None,
'datetime': None,
'nonce': None,
}
def fetch_balance(self, params={}):
"""获取账户余额"""
self.load_token()
request = {
'id': self.token,
}
response = 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' # 强制设定为 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)
result[currency] = {
'free': free_margin,
'used': margin,
'total': balance,
'equity': equity,
}
return self.safe_balance(result)
def fetch_account_details(self, params={}):
"""获取账户信息"""
self.load_token()
request = {
'id': self.token,
}
response = 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),
}
def fetch_open_orders(self, symbol=None, since=None, limit=None, params={}):
"""获取未平仓订单 - 修复版本"""
self.load_token()
request = {
'id': self.token,
}
response = 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)
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 = []
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,
'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}")
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)
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,
}
def create_order(self, symbol, type, side, amount, price=None, params={}):
"""创建订单"""
self.load_token()
market = self.market(symbol)
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 = self.private_get_ordersend(self.extend(request, params))
return self.parse_order(response, market)
def cancel_order(self, id, symbol=None, params={}):
"""取消订单"""
self.load_token()
request = {
'id': self.token,
'ticket': int(id),
}
response = 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):
"""签名请求"""
url = self.urls['api'][api] + '/' + path
query = self.omit(params, self.extract_params(path))
if method == 'GET' and query:
url += '?' + self.urlencode(query)
return {
'url': url,
'method': method,
'body': body,
'headers': headers
}