Compare commits

...

10 Commits

Author SHA1 Message Date
lz_db
be4c883d62 1 2025-12-02 15:17:58 +08:00
lz_db
502a0c49f2 处理时间差,统一UTC时间 2025-11-30 19:29:17 +08:00
lz_db
ef0058aded 1 2025-11-30 15:16:39 +08:00
lz_db
2f34f56527 ` 2025-11-30 14:47:29 +08:00
lz_db
a630c42ddd up 2025-11-30 11:19:39 +08:00
lz_db
6bb93a3968 update fetch_closed_orders 2025-11-30 08:08:29 +08:00
lz_db
0795f47168 1 2025-11-29 23:45:55 +08:00
lz_db
efd59755d8 1 2025-11-29 22:42:32 +08:00
macbook_max
b298c01743 mt5添加获取持仓方法 2025-11-29 20:49:42 +08:00
7LZL
dd4d01c58a 2 2025-11-23 21:17:08 +08:00
7 changed files with 5733 additions and 289 deletions

3
.gitignore vendored
View File

@@ -1,4 +1,5 @@
build/ build/
dist/ dist/
ccxt/__pycache__/ ccxt/__pycache__/
__pycache__/ __pycache__/
.vscode/settings.json

View File

@@ -1,4 +1,4 @@
Metadata-Version: 2.4 Metadata-Version: 2.1
Name: ccxt Name: ccxt
Version: 4.5.18 Version: 4.5.18
Summary: A cryptocurrency trading API with more than 100 exchanges in JavaScript / TypeScript / Python / C# / PHP / Go Summary: A cryptocurrency trading API with more than 100 exchanges in JavaScript / TypeScript / Python / C# / PHP / Go
@@ -32,34 +32,9 @@ Classifier: Programming Language :: PHP
Classifier: Operating System :: OS Independent Classifier: Operating System :: OS Independent
Classifier: Environment :: Console Classifier: Environment :: Console
Description-Content-Type: text/markdown Description-Content-Type: text/markdown
License-File: LICENSE.txt
Requires-Dist: setuptools>=60.9.0
Requires-Dist: certifi>=2018.1.18
Requires-Dist: requests>=2.18.4
Requires-Dist: cryptography>=2.6.1
Requires-Dist: typing_extensions>=4.4.0
Requires-Dist: aiohttp>=3.10.11; python_version >= "3.5.2"
Requires-Dist: aiodns>=1.1.1; python_version >= "3.5.2"
Requires-Dist: yarl>=1.7.2; python_version >= "3.5.2"
Requires-Dist: coincurve==21.0.0; python_version >= "3.9" and python_version <= "3.13"
Provides-Extra: qa Provides-Extra: qa
Requires-Dist: ruff==0.0.292; extra == "qa"
Requires-Dist: tox>=4.8.0; extra == "qa"
Provides-Extra: type Provides-Extra: type
Requires-Dist: mypy==1.6.1; extra == "type" License-File: LICENSE.txt
Dynamic: author
Dynamic: author-email
Dynamic: classifier
Dynamic: description
Dynamic: description-content-type
Dynamic: home-page
Dynamic: keywords
Dynamic: license
Dynamic: license-file
Dynamic: project-url
Dynamic: provides-extra
Dynamic: requires-dist
Dynamic: summary
# CCXT CryptoCurrency eXchange Trading Library # CCXT CryptoCurrency eXchange Trading Library

View File

@@ -19,6 +19,7 @@ from ccxt.base.errors import ExchangeNotAvailable
from ccxt.base.errors import InvalidNonce from ccxt.base.errors import InvalidNonce
from ccxt.base.decimal_to_precision import TICK_SIZE from ccxt.base.decimal_to_precision import TICK_SIZE
from ccxt.base.precise import Precise from ccxt.base.precise import Precise
from datetime import datetime, timedelta
class mt5(Exchange, ImplicitAPI): class mt5(Exchange, ImplicitAPI):
@@ -52,6 +53,7 @@ class mt5(Exchange, ImplicitAPI):
'fetchOHLCV': True, 'fetchOHLCV': True,
'fetchOpenOrders': True, 'fetchOpenOrders': True,
'fetchOrder': True, 'fetchOrder': True,
'fetchPositions': True,
'fetchOrderBook': True, 'fetchOrderBook': True,
'fetchTicker': True, 'fetchTicker': True,
'fetchTickers': True, 'fetchTickers': True,
@@ -154,6 +156,10 @@ class mt5(Exchange, ImplicitAPI):
response = await self.private_get_connect(request) response = await self.private_get_connect(request)
self.token = response self.token = response
self.diff_milliseconds = 0 # 重置时间差
# 获取服务器时区
await self.server_timezone()
return self.token return self.token
async def check_connect(self): async def check_connect(self):
@@ -177,28 +183,29 @@ class mt5(Exchange, ImplicitAPI):
self.token = None self.token = None
async def fetch_markets(self, params={}): async def fetch_markets(self, params={}):
"""获取交易对列表 - 异步修复版本""" """获取交易对列表 - 使用 SymbolParamsMany 接口"""
if not hasattr(self, 'token') or not self.token: if not hasattr(self, 'token') or not self.token:
await self.get_token() await self.get_token()
request = { request = {
'id': self.token, 'id': self.token,
'limit': 10000, # 添加 limit 参数获取所有交易对
} }
try: try:
response = await self.private_get_symbols(self.extend(request, params)) response = await self.private_get_symbolparamsmany(self.extend(request, params))
markets = [] markets = []
if isinstance(response, dict): if isinstance(response, list):
for symbol, info in response.items(): for symbol_data in response:
try: try:
market = self.parse_market(info) market = self.parse_market(symbol_data)
if market and market.get('symbol'): if market and market.get('symbol'):
markets.append(market) markets.append(market)
except Exception as e: except Exception as e:
# 跳过解析失败的市场,继续处理其他市场
if self.verbose: if self.verbose:
print(f"跳过交易对 {symbol}: {e}") symbol_name = self.safe_string(symbol_data, 'symbol', 'unknown')
print(f"跳过交易对 {symbol_name}: {e}")
continue continue
# 设置市场数据 # 设置市场数据
@@ -220,42 +227,72 @@ class mt5(Exchange, ImplicitAPI):
except Exception as e: except Exception as e:
raise ExchangeError(f"获取市场数据失败: {e}") raise ExchangeError(f"获取市场数据失败: {e}")
def parse_market(self, info): def parse_market(self, symbol_data):
"""解析市场信息 - 更健壮的版本""" """解析市场信息 - 根据新的数据格式"""
try: try:
# 安全获取 symbol if not isinstance(symbol_data, dict):
if not isinstance(info, dict):
return None return None
symbol = self.safe_string(info, 'currency', '') symbol = self.safe_string(symbol_data, 'symbol')
if not symbol: if not symbol:
return None return None
symbol = symbol.upper().strip() symbol_info = self.safe_dict(symbol_data, 'symbolInfo', {})
symbol_group = self.safe_dict(symbol_data, 'symbolGroup', {})
# 解析基础信息
symbol_name = symbol.upper().strip()
# 确保符号格式正确 (如 EURUSD) # 确保符号格式正确 (如 EURUSD)
if len(symbol) < 6: if len(symbol_name) < 6:
# 处理较短的符号 # 处理较短的符号
base = symbol base = symbol_name
quote = 'USD' # 默认报价货币 quote = 'USD' # 默认报价货币
else: else:
base = symbol[:3] base = symbol_name[:3]
quote = symbol[3:] quote = symbol_name[3:]
base = symbol[:3]
quote = symbol[3:]
# 安全处理精度 # 解析精度信息
digits = self.safe_integer(info, 'digits', 5) digits = self.safe_integer(symbol_info, 'digits', 5)
# 确保 digits 是整数
if digits is not None: if digits is not None:
try: try:
digits = int(digits) digits = int(digits)
except (ValueError, TypeError): except (ValueError, TypeError):
digits = 5 digits = 5
market_id = symbol # 解析合约大小
contract_size = self.safe_number(symbol_info, 'contractSize', 100000)
# 解析交易量限制
min_volume = self.safe_number(symbol_group, 'minVolume', 0.01)
max_volume = self.safe_number(symbol_group, 'maxVolume', 100)
volume_step = self.safe_number(symbol_group, 'volumeStep', 0.01)
# 处理最小交易量单位转换
# 如果 minVolume 很大,可能是以合约单位表示,需要转换为手数
if min_volume > 1000: # 假设大于1000的是合约单位
min_volume = min_volume / contract_size if contract_size > 0 else 0.01
# 解析价格精度
points = self.safe_number(symbol_info, 'points', 0.00001)
price_precision = digits
# 解析保证金信息
initial_margin = self.safe_number(symbol_group, 'initialMargin', 0)
maintenance_margin = self.safe_number(symbol_group, 'maintenanceMargin', 0)
# 解析货币信息
profit_currency = self.safe_string(symbol_info, 'profitCurrency', 'USD')
margin_currency = self.safe_string(symbol_info, 'marginCurrency', base)
# 解析交易模式
trade_mode = self.safe_string(symbol_group, 'tradeMode', 'Disabled')
active = trade_mode != 'Disabled'
# 解析描述信息
description = self.safe_string(symbol_info, 'description', '')
market_id = symbol_name
return { return {
'id': market_id, 'id': market_id,
@@ -264,33 +301,61 @@ class mt5(Exchange, ImplicitAPI):
'quote': quote, 'quote': quote,
'baseId': base, 'baseId': base,
'quoteId': quote, 'quoteId': quote,
'active': True, 'active': active,
'type': 'spot', 'type': 'spot',
'spot': True, 'spot': True,
'margin': True, 'margin': True,
'precision': { 'precision': {
'price': digits, 'price': price_precision,
'amount': 2, 'amount': 2, # 手数精度
'base': 2,
'quote': price_precision,
}, },
'limits': { 'limits': {
'amount': { 'amount': {
'min': self.safe_number(info, 'minVolume', 0.01), 'min': min_volume,
'max': self.safe_number(info, 'maxVolume'), 'max': max_volume,
}, },
'price': { 'price': {
'min': None, 'min': points, # 最小价格变动
'max': None, 'max': None,
}, },
'cost': { 'cost': {
'min': None, 'min': None,
'max': None, 'max': None,
}, },
'leverage': {
'min': 1.0,
'max': self.safe_number(symbol_group, 'accountLeverage', 100.0),
}
}, },
'info': info, 'contractSize': contract_size,
'expiry': None,
'expiryDatetime': None,
'strike': None,
'optionType': None,
'taker': self.safe_number(symbol_group, 'commission', 0), # 可能需要调整
'maker': self.safe_number(symbol_group, 'commission', 0), # 可能需要调整
'percentage': True,
'tierBased': False,
'feeSide': 'quote',
'info': symbol_data,
'margin': {
'initial': initial_margin,
'maintenance': maintenance_margin,
},
'swap': {
'long': self.safe_number(symbol_group, 'swapLong', 0),
'short': self.safe_number(symbol_group, 'swapShort', 0),
},
'lotSize': volume_step,
'minNotional': None,
'maxNotional': None,
} }
except Exception as e: except Exception as e:
if self.verbose: if self.verbose:
print(f"解析市场信息失败: {e}, info: {info}") symbol_name = self.safe_string(symbol_data, 'symbol', 'unknown')
print(f"解析市场信息失败 {symbol_name}: {e}")
return None return None
async def server_timezone(self): async def server_timezone(self):
@@ -306,6 +371,7 @@ class mt5(Exchange, ImplicitAPI):
} }
response = await self.private_get_servertimezone(request) response = await self.private_get_servertimezone(request)
self.timezone = int(float(response)) self.timezone = int(float(response))
self.diff_milliseconds = self.timezone * 3600 * 1000
return self.timezone return self.timezone
async def fetch_balance(self, params={}): async def fetch_balance(self, params={}):
@@ -502,14 +568,141 @@ class mt5(Exchange, ImplicitAPI):
return tickers return tickers
async def fetch_positions(self, symbol: Strings = None, params={}):
"""异步获取持仓信息"""
if not hasattr(self, 'token') or not self.token:
await self.get_token()
await self.server_timezone()
request = {
'id': self.token,
}
await self.load_markets()
symbol = self.market_symbols(symbol)
# print(symbol)
response = await self.private_get_openedorders(self.extend(request, params))
data = []
for item in response:
state = self.safe_string(item, 'state', "")
lots = self.safe_number(item, 'lots', 0)
close_lots = self.safe_number(item, 'closeLots', 0)
if state == "Filled" or lots <= close_lots:
data.append(item)
# 使用基类的 parse_positions 方法,让 parse_position 自己判断是否为有效持仓
result = self.parse_positions(data, symbol)
# 过滤掉 None 值(无效持仓)
result = [position for position in result if position is not None]
return result
def parse_position(self, position, market: Market = None):
"""解析持仓信息 - 根据真实数据调整"""
id = self.safe_string(position, 'ticket')
market_id = self.safe_string(position, 'symbol')
# 获取市场信息
symbol = self.safe_symbol(market_id, market, '/')
lots = self.safe_number(position, 'lots', 0)
close_lots = self.safe_number(position, 'closeLots', 0)
# 解析时间戳
timestamp = self.parse8601(self.safe_string(position, 'openTime')) - self.diff_milliseconds
open_timestamp_utc = self.safe_integer(position, 'openTimestampUTC') - self.diff_milliseconds
if open_timestamp_utc:
timestamp = open_timestamp_utc
# 确定持仓方向
order_type = self.safe_string(position, 'orderType')
side = 'long' if order_type == 'Buy' else 'short'
# 获取持仓数量 (未平仓数量)
contracts = lots - close_lots
# 获取价格信息
entry_price = self.safe_number(position, 'openPrice', 0)
current_price = self.safe_number(position, 'closePrice', entry_price)
mark_price = current_price
# 计算持仓价值
contract_size = self.safe_number(position, 'contractSize', 1.0)
notional = contracts * entry_price * contract_size if contracts and entry_price and contract_size else None
# 计算盈亏 - 使用 profit 字段
unrealized_pnl = self.safe_number(position, 'profit', 0)
# 计算保证金信息
initial_margin = None
initial_margin_percentage = None
# 如果有持仓价值,计算保证金
if notional is not None and notional != 0:
leverage = self.safe_number(position, 'leverage', 100)
initial_margin = notional / leverage
initial_margin_percentage = 1 / leverage
# 计算强平价格 (简化计算)
liquidation_price = None
if entry_price is not None:
if side == 'long':
liquidation_price = entry_price * 0.95 # 假设 5% 强平线
else:
liquidation_price = entry_price * 1.05 # 假设 5% 强平线
# 计算百分比盈亏
percentage = None
if entry_price is not None and mark_price is not None and entry_price != 0:
if side == 'long':
percentage = (mark_price - entry_price) / entry_price
else:
percentage = (entry_price - mark_price) / entry_price
# 获取止损止盈价格
stop_loss_price = self.safe_number(position, 'stopLoss')
take_profit_price = self.safe_number(position, 'takeProfit')
# 返回标准化的持仓信息
return {
'info': position,
'id': self.safe_string(position, 'ticket'),
'symbol': symbol,
'timestamp': timestamp,
'datetime': self.iso8601(timestamp) if timestamp else None,
'lastUpdateTimestamp': timestamp,
'initialMargin': initial_margin,
'initialMarginPercentage': initial_margin_percentage,
'maintenanceMargin': None,
'maintenanceMarginPercentage': None,
'entryPrice': entry_price,
'notional': notional,
'leverage': self.safe_number(position, 'leverage', 100),
'unrealizedPnl': unrealized_pnl,
'realizedPnl': self.safe_number(position, 'realizedPnl', 0),
'contracts': contracts,
'contractSize': contract_size,
'marginRatio': None,
'liquidationPrice': liquidation_price,
'markPrice': mark_price,
'lastPrice': mark_price,
'collateral': initial_margin,
'marginMode': 'cross',
'side': side,
'percentage': percentage,
'stopLossPrice': stop_loss_price,
'takeProfitPrice': take_profit_price,
'hedged': None,
}
async def fetch_open_orders(self, symbol=None, since=None, limit=None, params={}): async def fetch_open_orders(self, symbol=None, since=None, limit=None, params={}):
"""异步获取未平仓订单 - 修复版本""" """异步获取未平仓订单 - 修复版本"""
if not hasattr(self, 'token') or not self.token: if not hasattr(self, 'token') or not self.token:
await self.get_token() await self.get_token()
request = { request = {
'id': self.token, 'id': self.token,
} }
await self.server_timezone()
response = await self.private_get_openedorders(self.extend(request, params)) response = await self.private_get_openedorders(self.extend(request, params))
@@ -528,23 +721,35 @@ class mt5(Exchange, ImplicitAPI):
"""异步获取已平仓订单 - 修复版本""" """异步获取已平仓订单 - 修复版本"""
if not hasattr(self, 'token') or not self.token: if not hasattr(self, 'token') or not self.token:
await self.get_token() await self.get_token()
await self.server_timezone()
if params.get('from') is None or params.get('to') is None:
yesterday = datetime.now() - timedelta(days=3)
params['from'] = yesterday.strftime('%Y-%m-%d') + 'T00:00:00'
tomorrow = datetime.now() + timedelta(days=1)
params['to'] = tomorrow.strftime('%Y-%m-%d') + 'T00:00:00'
request = { request = {
'id': self.token, 'id': self.token,
'sort': 'OpenTime',
'ascending': 'true',
} }
response = await self.private_get_closedorders(self.extend(request, params)) response = await self.private_get_orderhistory(self.extend(request, params))
data = self.safe_list(response, 'orders')
# 如果指定了特定交易对,进行过滤 # 如果指定了特定交易对,进行过滤
if symbol is not None: if symbol is not None:
market = self.market(symbol) market = self.market(symbol)
filtered_orders = [] filtered_orders = []
for order in response: for order in data:
if isinstance(order, dict) and order.get('symbol') == market['id']: if isinstance(order, dict) and order.get('symbol') == market['id']:
filtered_orders.append(order) filtered_orders.append(order)
return self.parse_orders(filtered_orders, market, since, limit) return self.parse_orders(filtered_orders, market, since, limit)
else: else:
return self.parse_orders(response, None, since, limit) return self.parse_orders(data, None, since, limit)
def parse_order(self, order, market=None): def parse_order(self, order, market=None):
"""解析订单信息 - 修复市场符号问题""" """解析订单信息 - 修复市场符号问题"""
@@ -567,9 +772,14 @@ class mt5(Exchange, ImplicitAPI):
else: else:
# 如果无法解析,使用原始 market_id # 如果无法解析,使用原始 market_id
symbol = market_id symbol = market_id
timestamp = self.parse8601(self.safe_string(order, 'openTime')) timestamp = self.safe_integer(order, 'openTimestampUTC') - self.diff_milliseconds
last_trade_timestamp = self.parse8601(self.safe_string(order, 'closeTime')) last_trade_timestamp = self.safe_integer(order, 'closeTimestampUTC')
if last_trade_timestamp is None or last_trade_timestamp <= 0:
last_trade_timestamp = timestamp
else:
last_trade_timestamp = last_trade_timestamp - self.diff_milliseconds
status = self.parse_order_status(self.safe_string(order, 'state')) status = self.parse_order_status(self.safe_string(order, 'state'))
side = self.parse_order_side(self.safe_string(order, 'orderType')) side = self.parse_order_side(self.safe_string(order, 'orderType'))
@@ -600,7 +810,7 @@ class mt5(Exchange, ImplicitAPI):
return self.safe_order({ return self.safe_order({
'id': id, 'id': id,
'clientOrderId': None, 'clientOrderId': self.safe_string(order, 'comment'),
'datetime': self.iso8601(timestamp), 'datetime': self.iso8601(timestamp),
'timestamp': timestamp, 'timestamp': timestamp,
'lastTradeTimestamp': last_trade_timestamp, 'lastTradeTimestamp': last_trade_timestamp,
@@ -640,13 +850,22 @@ class mt5(Exchange, ImplicitAPI):
def parse_order_status(self, status): def parse_order_status(self, status):
statuses = { statuses = {
'Started': 'open', # MT5 状态 -> CCXT 标准状态
'Placed': 'open', 'Started': 'open', # 订单已开始
'Cancelled': 'canceled', 'Placed': 'open', # 订单已放置
'Partial': 'open', 'RequestAdding': 'pending', # 请求添加订单(待处理)
'Filled': 'closed', 'RequestModifying': 'pending', # 请求修改订单(待处理)
'Rejected': 'rejected', 'RequestCancelling': 'pending', # 请求取消订单(待处理)
'Expired': 'expired', 'Partial': 'open', # 订单部分成交(仍可继续成交)
'Filled': 'closed', # 订单完全成交
'Cancelled': 'canceled', # 订单已取消
'Rejected': 'rejected', # 订单被拒绝
'Expired': 'expired', # 订单已过期
# 备用映射
'New': 'open',
'Active': 'open',
'Done': 'closed',
'Canceled': 'canceled',
} }
return self.safe_string(statuses, status, status) return self.safe_string(statuses, status, status)
@@ -672,68 +891,6 @@ class mt5(Exchange, ImplicitAPI):
} }
return self.safe_string(types, type, type) 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={}): async def create_order(self, symbol, type, side, amount, price=None, params={}):
"""创建订单""" """创建订单"""
await self.load_markets() await self.load_markets()

View File

@@ -24,6 +24,7 @@ from ccxt.base.errors import InvalidNonce
from ccxt.base.errors import RequestTimeout from ccxt.base.errors import RequestTimeout
from ccxt.base.decimal_to_precision import TICK_SIZE from ccxt.base.decimal_to_precision import TICK_SIZE
from ccxt.base.precise import Precise from ccxt.base.precise import Precise
from datetime import datetime, timedelta
class mt5(Exchange, ImplicitAPI): class mt5(Exchange, ImplicitAPI):
@@ -58,6 +59,7 @@ class mt5(Exchange, ImplicitAPI):
'fetchOHLCV': True, 'fetchOHLCV': True,
'fetchOpenOrders': True, 'fetchOpenOrders': True,
'fetchOrder': True, 'fetchOrder': True,
'fetchPositions': True,
'fetchOrderBook': True, 'fetchOrderBook': True,
'fetchTicker': True, 'fetchTicker': True,
'fetchTickers': True, 'fetchTickers': True,
@@ -168,6 +170,8 @@ class mt5(Exchange, ImplicitAPI):
response = self.private_get_connect(params) response = self.private_get_connect(params)
self.token = response self.token = response
self.diff_milliseconds = 0 # 服务器时区差异,默认设置为 0
self.server_timezone()
return self.token return self.token
def check_connect(self): def check_connect(self):
@@ -206,28 +210,31 @@ class mt5(Exchange, ImplicitAPI):
} }
response = self.private_get_servertimezone(request) response = self.private_get_servertimezone(request)
self.timezone = int(float(response)) self.timezone = int(float(response))
self.diff_milliseconds = self.timezone * 3600 * 1000
return self.timezone return self.timezone
def fetch_markets(self, params={}): def fetch_markets(self, params={}):
"""获取交易对列表 - 修复版本""" """获取交易对列表 - 使用 SymbolParamsMany 接口"""
self.load_token() self.load_token()
request = { request = {
'id': self.token, 'id': self.token,
'limit': 10000, # 添加 limit 参数获取所有交易对
} }
try: try:
response = self.private_get_symbols(self.extend(request, params)) response = self.private_get_symbolparamsmany(self.extend(request, params))
markets = [] markets = []
if isinstance(response, dict): if isinstance(response, list):
for symbol, info in response.items(): for symbol_data in response:
try: try:
market = self.parse_market(info) market = self.parse_market(symbol_data)
if market and market.get('symbol'): if market and market.get('symbol'):
markets.append(market) markets.append(market)
except Exception as e: except Exception as e:
if self.verbose: if self.verbose:
print(f"跳过交易对 {symbol}: {e}") symbol_name = self.safe_string(symbol_data, 'symbol', 'unknown')
print(f"跳过交易对 {symbol_name}: {e}")
continue continue
# 设置市场数据 # 设置市场数据
@@ -249,34 +256,72 @@ class mt5(Exchange, ImplicitAPI):
except Exception as e: except Exception as e:
raise ExchangeError(f"获取市场数据失败: {e}") raise ExchangeError(f"获取市场数据失败: {e}")
def parse_market(self, info): def parse_market(self, symbol_data):
"""解析市场信息 - 更健壮的版本""" """解析市场信息 - 根据新的数据格式"""
try: try:
if not isinstance(info, dict): if not isinstance(symbol_data, dict):
return None return None
symbol = self.safe_string(info, 'currency', '') symbol = self.safe_string(symbol_data, 'symbol')
if not symbol: if not symbol:
return None return None
symbol = symbol.upper().strip() symbol_info = self.safe_dict(symbol_data, 'symbolInfo', {})
symbol_group = self.safe_dict(symbol_data, 'symbolGroup', {})
# 处理符号格式 # 解析基础信息
if len(symbol) < 6: symbol_name = symbol.upper().strip()
base = symbol
quote = 'USD' # 确保符号格式正确 (如 EURUSD)
if len(symbol_name) < 6:
# 处理较短的符号
base = symbol_name
quote = 'USD' # 默认报价货币
else: else:
base = symbol[:3] base = symbol_name[:3]
quote = symbol[3:] quote = symbol_name[3:]
digits = self.safe_integer(info, 'digits', 5) # 解析精度信息
digits = self.safe_integer(symbol_info, 'digits', 5)
if digits is not None: if digits is not None:
try: try:
digits = int(digits) digits = int(digits)
except (ValueError, TypeError): except (ValueError, TypeError):
digits = 5 digits = 5
market_id = symbol # 解析合约大小
contract_size = self.safe_number(symbol_info, 'contractSize', 100000)
# 解析交易量限制
min_volume = self.safe_number(symbol_group, 'minVolume', 0.01)
max_volume = self.safe_number(symbol_group, 'maxVolume', 100)
volume_step = self.safe_number(symbol_group, 'volumeStep', 0.01)
# 处理最小交易量单位转换
# 如果 minVolume 很大,可能是以合约单位表示,需要转换为手数
if min_volume > 1000: # 假设大于1000的是合约单位
min_volume = min_volume / contract_size if contract_size > 0 else 0.01
# 解析价格精度
points = self.safe_number(symbol_info, 'points', 0.00001)
price_precision = digits
# 解析保证金信息
initial_margin = self.safe_number(symbol_group, 'initialMargin', 0)
maintenance_margin = self.safe_number(symbol_group, 'maintenanceMargin', 0)
# 解析货币信息
profit_currency = self.safe_string(symbol_info, 'profitCurrency', 'USD')
margin_currency = self.safe_string(symbol_info, 'marginCurrency', base)
# 解析交易模式
trade_mode = self.safe_string(symbol_group, 'tradeMode', 'Disabled')
active = trade_mode != 'Disabled'
# 解析描述信息
description = self.safe_string(symbol_info, 'description', '')
market_id = symbol_name
return { return {
'id': market_id, 'id': market_id,
@@ -285,35 +330,63 @@ class mt5(Exchange, ImplicitAPI):
'quote': quote, 'quote': quote,
'baseId': base, 'baseId': base,
'quoteId': quote, 'quoteId': quote,
'active': True, 'active': active,
'type': 'spot', 'type': 'spot',
'spot': True, 'spot': True,
'margin': True, 'margin': True,
'precision': { 'precision': {
'price': digits, 'price': price_precision,
'amount': 2, 'amount': 2, # 手数精度
'base': 2,
'quote': price_precision,
}, },
'limits': { 'limits': {
'amount': { 'amount': {
'min': self.safe_number(info, 'minVolume', 0.01), 'min': min_volume,
'max': self.safe_number(info, 'maxVolume'), 'max': max_volume,
}, },
'price': { 'price': {
'min': None, 'min': points, # 最小价格变动
'max': None, 'max': None,
}, },
'cost': { 'cost': {
'min': None, 'min': None,
'max': None, 'max': None,
}, },
'leverage': {
'min': 1.0,
'max': self.safe_number(symbol_group, 'accountLeverage', 100.0),
}
}, },
'info': info, 'contractSize': contract_size,
'expiry': None,
'expiryDatetime': None,
'strike': None,
'optionType': None,
'taker': self.safe_number(symbol_group, 'commission', 0), # 可能需要调整
'maker': self.safe_number(symbol_group, 'commission', 0), # 可能需要调整
'percentage': True,
'tierBased': False,
'feeSide': 'quote',
'info': symbol_data,
'margin': {
'initial': initial_margin,
'maintenance': maintenance_margin,
},
'swap': {
'long': self.safe_number(symbol_group, 'swapLong', 0),
'short': self.safe_number(symbol_group, 'swapShort', 0),
},
'lotSize': volume_step,
'minNotional': None,
'maxNotional': None,
} }
except Exception as e: except Exception as e:
if self.verbose: if self.verbose:
print(f"解析市场信息失败: {e}, info: {info}") symbol_name = self.safe_string(symbol_data, 'symbol', 'unknown')
print(f"解析市场信息失败 {symbol_name}: {e}")
return None return None
def fetch_ticker(self, symbol, params={}): def fetch_ticker(self, symbol, params={}):
"""获取行情数据""" """获取行情数据"""
self.load_token() self.load_token()
@@ -514,9 +587,138 @@ class mt5(Exchange, ImplicitAPI):
'isInvestor': self.safe_value(response, 'isInvestor', False), 'isInvestor': self.safe_value(response, 'isInvestor', False),
} }
def fetch_positions(self, symbol: Strings = None, params={}):
"""异步获取持仓信息"""
if not hasattr(self, 'token') or not self.token:
self.get_token()
self.server_timezone()
request = {
'id': self.token,
}
self.load_markets()
symbol = self.market_symbols(symbol)
# print(symbol)
response = self.private_get_openedorders(self.extend(request, params))
data = []
for item in response:
state = self.safe_string(item, 'state', "")
lots = self.safe_number(item, 'lots', 0)
close_lots = self.safe_number(item, 'closeLots', 0)
if state == "Filled" or lots <= close_lots:
data.append(item)
# 使用基类的 parse_positions 方法,让 parse_position 自己判断是否为有效持仓
result = self.parse_positions(data, symbol)
# 过滤掉 None 值(无效持仓)
result = [position for position in result if position is not None]
return result
def parse_position(self, position, market: Market = None):
"""解析持仓信息 - 根据真实数据调整"""
id = self.safe_string(position, 'ticket')
market_id = self.safe_string(position, 'symbol')
# 获取市场信息
symbol = self.safe_symbol(market_id, market, '/')
lots = self.safe_number(position, 'lots', 0)
close_lots = self.safe_number(position, 'closeLots', 0)
# 解析时间戳
timestamp = self.parse8601(self.safe_string(position, 'openTime')) - self.diff_milliseconds
open_timestamp_utc = self.safe_integer(position, 'openTimestampUTC') - self.diff_milliseconds
if open_timestamp_utc:
timestamp = open_timestamp_utc
# 确定持仓方向
order_type = self.safe_string(position, 'orderType')
if order_type in ['Buy','Sell']:
side = 'long' if order_type == 'Buy' else 'short'
else:
side = order_type
# 获取持仓数量 (未平仓数量)
contracts = lots - close_lots
# 获取价格信息
entry_price = self.safe_number(position, 'openPrice', 0)
current_price = self.safe_number(position, 'closePrice', entry_price)
mark_price = current_price
# 计算持仓价值
contract_size = self.safe_number(position, 'contractSize', 1.0)
notional = contracts * entry_price * contract_size if contracts and entry_price and contract_size else None
# 计算盈亏 - 使用 profit 字段
unrealized_pnl = self.safe_number(position, 'profit', 0)
# 计算保证金信息
initial_margin = None
initial_margin_percentage = None
# 如果有持仓价值,计算保证金
if notional is not None and notional != 0:
leverage = self.safe_number(position, 'leverage', 100)
initial_margin = notional / leverage
initial_margin_percentage = 1 / leverage
# 计算强平价格 (简化计算)
liquidation_price = None
if entry_price is not None:
if side == 'long':
liquidation_price = entry_price * 0.95 # 假设 5% 强平线
else:
liquidation_price = entry_price * 1.05 # 假设 5% 强平线
# 计算百分比盈亏
percentage = None
if entry_price is not None and mark_price is not None and entry_price != 0:
if side == 'long':
percentage = (mark_price - entry_price) / entry_price
else:
percentage = (entry_price - mark_price) / entry_price
# 获取止损止盈价格
stop_loss_price = self.safe_number(position, 'stopLoss')
take_profit_price = self.safe_number(position, 'takeProfit')
# 返回标准化的持仓信息
return {
'info': position,
'id': self.safe_string(position, 'ticket'),
'symbol': symbol,
'timestamp': timestamp,
'datetime': self.iso8601(timestamp) if timestamp else None,
'lastUpdateTimestamp': timestamp,
'initialMargin': initial_margin,
'initialMarginPercentage': initial_margin_percentage,
'maintenanceMargin': None,
'maintenanceMarginPercentage': None,
'entryPrice': entry_price,
'notional': notional,
'leverage': self.safe_number(position, 'leverage', 100),
'unrealizedPnl': unrealized_pnl,
'realizedPnl': self.safe_number(position, 'realizedPnl', 0),
'contracts': contracts,
'contractSize': contract_size,
'marginRatio': None,
'liquidationPrice': liquidation_price,
'markPrice': mark_price,
'lastPrice': mark_price,
'collateral': initial_margin,
'marginMode': 'cross',
'side': side,
'percentage': percentage,
'stopLossPrice': stop_loss_price,
'takeProfitPrice': take_profit_price,
'hedged': None,
}
def fetch_open_orders(self, symbol=None, since=None, limit=None, params={}): def fetch_open_orders(self, symbol=None, since=None, limit=None, params={}):
"""获取未平仓订单""" """获取未平仓订单"""
self.load_token() self.load_token()
self.server_timezone()
request = { request = {
'id': self.token, 'id': self.token,
} }
@@ -536,21 +738,32 @@ class mt5(Exchange, ImplicitAPI):
def fetch_closed_orders(self, symbol=None, since=None, limit=None, params={}): def fetch_closed_orders(self, symbol=None, since=None, limit=None, params={}):
"""获取已平仓订单""" """获取已平仓订单"""
self.load_token() self.load_token()
self.server_timezone()
if params.get('from') is None or params.get('to') is None:
yesterday = datetime.now() - timedelta(days=3)
params['from'] = yesterday.strftime('%Y-%m-%d') + 'T00:00:00'
tomorrow = datetime.now() + timedelta(days=1)
params['to'] = tomorrow.strftime('%Y-%m-%d') + 'T00:00:00'
request = { request = {
'id': self.token, 'id': self.token,
'sort': 'OpenTime',
'ascending': 'true',
} }
response = self.private_get_closedorders(self.extend(request, params)) response = self.private_get_orderhistory(self.extend(request, params))
data = self.safe_list(response, 'orders')
if symbol is not None: if symbol is not None:
market = self.market(symbol) market = self.market(symbol)
filtered_orders = [] filtered_orders = []
for order in response: for order in data:
if isinstance(order, dict) and order.get('symbol') == market['id']: if isinstance(order, dict) and order.get('symbol') == market['id']:
filtered_orders.append(order) filtered_orders.append(order)
return self.parse_orders(filtered_orders, market, since, limit) return self.parse_orders(filtered_orders, market, since, limit)
else: else:
return self.parse_orders(response, None, since, limit) return self.parse_orders(data, None, since, limit)
def parse_order(self, order, market=None): def parse_order(self, order, market=None):
"""解析订单信息""" """解析订单信息"""
@@ -569,9 +782,14 @@ class mt5(Exchange, ImplicitAPI):
else: else:
symbol = market_id symbol = market_id
timestamp = self.parse8601(self.safe_string(order, 'openTime')) timestamp = self.safe_integer(order, 'openTimestampUTC') - self.diff_milliseconds
last_trade_timestamp = self.parse8601(self.safe_string(order, 'closeTime')) last_trade_timestamp = self.safe_integer(order, 'closeTimestampUTC')
if last_trade_timestamp is None or last_trade_timestamp <= 0:
last_trade_timestamp = timestamp
else:
last_trade_timestamp = last_trade_timestamp - self.diff_milliseconds
status = self.parse_order_status(self.safe_string(order, 'state')) status = self.parse_order_status(self.safe_string(order, 'state'))
side = self.parse_order_side(self.safe_string(order, 'orderType')) side = self.parse_order_side(self.safe_string(order, 'orderType'))
type = self.parse_order_type(self.safe_string(order, 'orderType')) type = self.parse_order_type(self.safe_string(order, 'orderType'))
@@ -598,7 +816,7 @@ class mt5(Exchange, ImplicitAPI):
return self.safe_order({ return self.safe_order({
'id': id, 'id': id,
'clientOrderId': None, 'clientOrderId': self.safe_string(order, 'comment'),
'datetime': self.iso8601(timestamp), 'datetime': self.iso8601(timestamp),
'timestamp': timestamp, 'timestamp': timestamp,
'lastTradeTimestamp': last_trade_timestamp, 'lastTradeTimestamp': last_trade_timestamp,
@@ -630,13 +848,22 @@ class mt5(Exchange, ImplicitAPI):
def parse_order_status(self, status): def parse_order_status(self, status):
statuses = { statuses = {
'Started': 'open', # MT5 状态 -> CCXT 标准状态
'Placed': 'open', 'Started': 'open', # 订单已开始
'Cancelled': 'canceled', 'Placed': 'open', # 订单已放置
'Partial': 'open', 'RequestAdding': 'pending', # 请求添加订单(待处理)
'Filled': 'closed', 'RequestModifying': 'pending', # 请求修改订单(待处理)
'Rejected': 'rejected', 'RequestCancelling': 'pending', # 请求取消订单(待处理)
'Expired': 'expired', 'Partial': 'open', # 订单部分成交(仍可继续成交)
'Filled': 'closed', # 订单完全成交
'Cancelled': 'canceled', # 订单已取消
'Rejected': 'rejected', # 订单被拒绝
'Expired': 'expired', # 订单已过期
# 备用映射
'New': 'open',
'Active': 'open',
'Done': 'closed',
'Canceled': 'canceled',
} }
return self.safe_string(statuses, status, status) return self.safe_string(statuses, status, status)
@@ -662,64 +889,6 @@ class mt5(Exchange, ImplicitAPI):
} }
return self.safe_string(types, type, type) return self.safe_string(types, type, type)
def parse_position(self, order_data, market: Market = None):
"""从订单数据解析持仓"""
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,
'unrealizedPnl': self.safe_number(order_data, 'profit', 0),
'realizedPnl': 0,
'liquidationPrice': None,
'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={}): def create_order(self, symbol, type, side, amount, price=None, params={}):
"""创建订单""" """创建订单"""
self.load_token() self.load_token()

View File

@@ -67,6 +67,7 @@ class mt5(mt5Parent):
"""监听订单变化""" """监听订单变化"""
if not hasattr(self, 'token') or not self.token: if not hasattr(self, 'token') or not self.token:
await self.get_token() await self.get_token()
await self.server_timezone()
ws_url = self.implode_hostname(self.urls['api']['ws']) ws_url = self.implode_hostname(self.urls['api']['ws'])
url = ws_url + '/OnOrderUpdate?id=' + self.token url = ws_url + '/OnOrderUpdate?id=' + self.token
message_hash = 'orders' message_hash = 'orders'
@@ -92,6 +93,7 @@ class mt5(mt5Parent):
"""监听持仓变化""" """监听持仓变化"""
if not hasattr(self, 'token') or not self.token: if not hasattr(self, 'token') or not self.token:
await self.get_token() await self.get_token()
await self.server_timezone()
ws_url = self.implode_hostname(self.urls['api']['ws']) ws_url = self.implode_hostname(self.urls['api']['ws'])
url = ws_url + '/OnOrderUpdate?id=' + self.token url = ws_url + '/OnOrderUpdate?id=' + self.token
message_hash = 'positions' message_hash = 'positions'
@@ -108,7 +110,7 @@ class mt5(mt5Parent):
positions = [position for position in positions if position['timestamp'] >= since] positions = [position for position in positions if position['timestamp'] >= since]
if limit is not None: if limit is not None:
positions = positions[-limit:] positions = positions[-limit:]
return positions return positions
async def watch_ticker(self, symbol: str, params={}): async def watch_ticker(self, symbol: str, params={}):
@@ -365,7 +367,7 @@ class mt5(mt5Parent):
"""处理 OrderUpdate 类型消息(包含订单、持仓、余额)""" """处理 OrderUpdate 类型消息(包含订单、持仓、余额)"""
try: try:
data = self.safe_value(message, 'data', {}) data = self.safe_value(message, 'data', {})
timestamp = self.safe_integer(message, 'timestampUTC') timestamp = self.safe_integer(message, 'timestampUTC') - self.diff_milliseconds
# 1. 解析余额信息 # 1. 解析余额信息
balance_data = self.parse_ws_balance_from_data(data) balance_data = self.parse_ws_balance_from_data(data)
@@ -382,6 +384,7 @@ class mt5(mt5Parent):
update_data = self.safe_value(data, 'update', {}) update_data = self.safe_value(data, 'update', {})
order_data = self.safe_value(update_data, 'order') order_data = self.safe_value(update_data, 'order')
if order_data: if order_data:
order_data['update_type'] = self.safe_value(update_data, 'type', None) # 这个字段可以判断是开仓还是平仓
order = self.parse_ws_order(order_data) order = self.parse_ws_order(order_data)
if order: if order:
# 使用简单的列表而不是 ArrayCacheBySymbolById # 使用简单的列表而不是 ArrayCacheBySymbolById
@@ -426,7 +429,7 @@ class mt5(mt5Parent):
"""处理 OpenedOrders 类型消息(只包含持仓)""" """处理 OpenedOrders 类型消息(只包含持仓)"""
try: try:
data = self.safe_value(message, 'data', []) data = self.safe_value(message, 'data', [])
timestamp = self.safe_integer(message, 'timestampUTC') timestamp = self.safe_integer(message, 'timestampUTC') - self.diff_milliseconds
# 解析持仓信息 # 解析持仓信息
positions = self.parse_ws_positions_from_orders(data) positions = self.parse_ws_positions_from_orders(data)
@@ -601,7 +604,7 @@ class mt5(mt5Parent):
"""解析单个订单数据""" """解析单个订单数据"""
if not order_data: if not order_data:
return None return None
# print("++++++",order_data)
try: try:
symbol = self.safe_string(order_data, 'symbol') symbol = self.safe_string(order_data, 'symbol')
if symbol and len(symbol) >= 6: if symbol and len(symbol) >= 6:
@@ -617,21 +620,37 @@ class mt5(mt5Parent):
close_time = self.safe_string(order_data, 'closeTime') close_time = self.safe_string(order_data, 'closeTime')
is_closed = close_time and close_time != "0001-01-01T00:00:00" is_closed = close_time and close_time != "0001-01-01T00:00:00"
timestamp = self.parse8601(self.safe_string(order_data, 'openTime')) timestamp = self.safe_integer(order_data, 'openTimestampUTC') - self.diff_milliseconds
if timestamp is None: last_trade_timestamp = self.safe_integer(order_data, 'closeTimestampUTC')
timestamp = self.milliseconds()
if last_trade_timestamp is None or last_trade_timestamp <= 0:
last_trade_timestamp = None last_trade_timestamp = timestamp
if is_closed: else:
last_trade_timestamp = self.parse8601(close_time) last_trade_timestamp = last_trade_timestamp - self.diff_milliseconds
mt5_order_type = self.safe_string(order_data, 'update_type', None)
amount = self.safe_number(order_data, 'lots', 0) amount = self.safe_number(order_data, 'lots', 0)
filled = self.safe_number(order_data, 'closeLots', 0) filled = self.safe_number(order_data, 'closeLots', 0)
price = self.safe_number(order_data, 'openPrice')
side = self.parse_order_side(self.safe_string(order_data, 'orderType'))
if mt5_order_type == 'MarketOpen':
amount = self.safe_number(order_data, 'lots', 0)
filled = self.safe_number(order_data, 'lots', 0)
elif mt5_order_type == 'MarketClose':
amount = self.safe_number(self.safe_dict(order_data, 'dealInternalIn', {}), 'lots', 0)
filled = self.safe_number(order_data, 'closeLots', 0)
price = self.safe_number(order_data, 'closePrice')
if side == 'buy':
side = 'sell'
else:
side = 'buy'
remaining = max(amount - filled, 0) if amount is not None and filled is not None else None remaining = max(amount - filled, 0) if amount is not None and filled is not None else None
return { return {
'id': self.safe_string(order_data, 'ticket'), 'id': self.safe_string(order_data, 'ticket'),
'clientOrderId': None, 'clientOrderId': self.safe_string(order_data, 'comment'),
'datetime': self.iso8601(timestamp), 'datetime': self.iso8601(timestamp),
'timestamp': timestamp, 'timestamp': timestamp,
'lastTradeTimestamp': last_trade_timestamp, 'lastTradeTimestamp': last_trade_timestamp,
@@ -641,8 +660,8 @@ class mt5(mt5Parent):
'type': self.parse_order_type(self.safe_string(order_data, 'orderType')), 'type': self.parse_order_type(self.safe_string(order_data, 'orderType')),
'timeInForce': None, 'timeInForce': None,
'postOnly': None, 'postOnly': None,
'side': self.parse_order_side(self.safe_string(order_data, 'orderType')), 'side': side,
'price': self.safe_number(order_data, 'openPrice'), 'price': price,
'stopLossPrice': self.safe_number(order_data, 'stopLoss'), 'stopLossPrice': self.safe_number(order_data, 'stopLoss'),
'takeProfitPrice': self.safe_number(order_data, 'takeProfit'), 'takeProfitPrice': self.safe_number(order_data, 'takeProfit'),
'reduceOnly': None, 'reduceOnly': None,
@@ -698,19 +717,27 @@ class mt5(mt5Parent):
quote = symbol[3:] quote = symbol[3:]
symbol = base + '/' + quote symbol = base + '/' + quote
timestamp = self.parse8601(self.safe_string(order_data, 'openTime')) timestamp = self.safe_integer(order_data, 'openTimestampUTC') - self.diff_milliseconds
contracts = self.safe_number(order_data, 'lots') contracts = self.safe_number(order_data, 'lots')
entry_price = self.safe_number(order_data, 'openPrice') entry_price = self.safe_number(order_data, 'openPrice')
mark_price = self.safe_number(order_data, 'closePrice') mark_price = self.safe_number(order_data, 'closePrice')
notional = contracts * entry_price if contracts and entry_price else None notional = contracts * entry_price if contracts and entry_price else None
# 获取止损止盈价格
stop_loss_price = self.safe_number(order_data, 'stopLoss')
take_profit_price = self.safe_number(order_data, 'takeProfit')
# 确定持仓方向
order_type = self.safe_string(order_data, 'orderType')
side = 'long' if order_type == 'Buy' else 'short'
return { return {
'id': self.safe_string(order_data, 'ticket'), 'id': self.safe_string(order_data, 'ticket'),
'symbol': symbol, 'symbol': symbol,
'timestamp': timestamp, 'timestamp': timestamp,
'datetime': self.iso8601(timestamp), 'datetime': self.iso8601(timestamp),
'side': self.parse_order_side(self.safe_string(order_data, 'orderType')), 'side': side,
'contracts': contracts, 'contracts': contracts,
'contractSize': self.safe_number(order_data, 'contractSize', 1.0), 'contractSize': self.safe_number(order_data, 'contractSize', 1.0),
'entryPrice': entry_price, 'entryPrice': entry_price,
@@ -728,9 +755,14 @@ class mt5(mt5Parent):
'initialMarginPercentage': None, 'initialMarginPercentage': None,
'maintenanceMargin': None, 'maintenanceMargin': None,
'maintenanceMarginPercentage': None, 'maintenanceMarginPercentage': None,
'stopLossPrice': stop_loss_price,
'takeProfitPrice': take_profit_price,
'lastPrice': mark_price,
'hedged': None,
'info': order_data, 'info': order_data,
} }
def parse_ws_ohlcv_message(self, data): def parse_ws_ohlcv_message(self, data):
"""解析WebSocket K线数据""" """解析WebSocket K线数据"""
timestamp = self.parse8601(self.safe_string(data, 'time')) timestamp = self.parse8601(self.safe_string(data, 'time'))

5072
test/data.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -62,39 +62,56 @@ async def quick_order_test():
logger.info("🚀 快速订单测试开始") logger.info("🚀 快速订单测试开始")
exchange = mt5({ exchange = mt5({
'user': 62333850, 'apiKey': '76888962',
'password': 'tecimil4', 'secret': 'LZ-trade666888',
'host': '78.140.180.198', 'verbose': False, # 启用详细日志
'port': 443, 'hostname': '43.133.21.145:5000',
'sandbox': True, 'options': {
# 'server': '147.160.254.81:443', # 使用服务器名称
# 或者
'host': '18.163.85.196',
'port': 443,
},
}) })
try: try:
# 测试连接 # 测试连接
balance = await exchange.fetch_balance() # balance = await exchange.fetch_balance()
logger.info(f"✅ 连接成功,余额: {balance['total'].get('USD', 'N/A')}") # logger.info(f"✅ 连接成功,余额: {balance}")
positions = await exchange.fetch_positions()
logger.info(f"✅ 连接成功,信息: {positions}")
# 获取市场信息 # 获取市场信息
markets = await exchange.fetch_markets() # markets = await exchange.fetch_markets()
logger.info(f"✅ 获取到 {len(markets)} 个交易对") # logger.info(f"✅ 获取到 {len(markets)} 个交易对")
# 获取当前价格 # 获取当前价格
ticker = await exchange.fetch_ticker('EUR/USD') # ticker = await exchange.fetch_ticker('EUR/USD')
logger.info(f"✅ EUR/USD 当前价格: 买={ticker['bid']}, 卖={ticker['ask']}") # logger.info(f"✅ EUR/USD 当前价格: 买={ticker['bid']}, 卖={ticker['ask']}")
# 获取订单簿 # 获取订单簿
orderbook = await exchange.fetch_order_book('EUR/USD') # orderbook = await exchange.fetch_order_book('EUR/USD')
logger.info(f"✅ EUR/USD 订单簿深度: {len(orderbook['bids'])} 买单, {len(orderbook['asks'])} 卖单") # logger.info(f"✅ EUR/USD 订单簿深度: {len(orderbook['bids'])} 买单, {len(orderbook['asks'])} 卖单")
# 获取开单 # 获取开单
open_orders = await exchange.fetch_open_orders() # open_orders = await exchange.fetch_open_orders()
logger.info(f"✅ 当前开单数量: {len(open_orders)}") # logger.info(f"✅ 当前开单数量: {len(open_orders)}")
# 获取订单
# closed_orders = await exchange.fetch_closed_orders()
# for order in closed_orders:
# # del order['info']
# logger.info(f"✅ 历史订单: {order}")
# logger.info(f"✅ 当前订单: {closed_orders}")
for order in open_orders: # for order in open_orders:
logger.info(f" 订单 {order['id']}: {order['symbol']} {order['side']} {order['type']} {order['status']}") # logger.info(f" 订单 {order['id']}: {order['symbol']} {order['side']} {order['type']} {order['status']}")
except Exception as e: except Exception as e:
logger.error(f"❌ 测试失败: {e}") logger.error(f"❌ 测试失败: {e}")
# 抛出异常,终止程序
raise e
finally: finally:
await exchange.close() await exchange.close()
@@ -107,37 +124,57 @@ async def websocket_quick_test():
'apiKey': '76888962', 'apiKey': '76888962',
'secret': 'LZ-trade666888', 'secret': 'LZ-trade666888',
'verbose': False, # 启用详细日志 'verbose': False, # 启用详细日志
# 'debug': True, # 启用详细调试信息 'hostname': '43.133.21.145:5000',
'hostname': '43.167.188.220:5000', 'options': {
'host': '18.163.85.196', # 'server': '147.160.254.81:443', # 使用服务器名称
'port': 443, # 或者
'host': '18.163.85.196',
'port': 443,
},
}) })
try: try:
# 监听all
async def all_listener():
while True:
print("111111111")
res = await exchange.watch_all()
print("==========================收到信息")
print(f"收到数据:{res}")
# 监听订单更新 # 监听订单更新
async def order_listener(): async def order_listener():
while True: while True:
# print("111111111") print("111111111")
res = await exchange.watch_ticker(symbol='BTCUSD') res = await exchange.watch_orders()
logger.error("aaa")
logger.warning("bbb")
logger.debug("ccc")
print("===========================收到信息") print("===========================收到信息")
print(res) for order in res:
# for order in res: # del order['info']
# logger.info(f"📦 订单更新: {order}") logger.info(f"📦 订单更新: {order}")
print(order)
# 监听持仓更新
async def positions_listener():
while True:
res = await exchange.watch_positions()
print("===========================收到信息")
print(len(res))
for position in res:
# del position['info']
logger.info(f"📦 持仓更新: {position}")
# 监听余额更新 # 监听余额更新
async def balance_listener(): # async def balance_listener():
balance = await exchange.watch_balance() # balance = await exchange.watch_balance()
total = sum([v for v in balance['total'].values() if v is not None]) # total = sum([v for v in balance['total'].values() if v is not None])
logger.info(f"💰 余额更新: 总余额 {total:.2f}") # logger.info(f"💰 余额更新: 总余额 {total:.2f}")
# 运行监听器 # 运行监听器
await asyncio.gather( await asyncio.gather(
# all_listener(),
order_listener(), order_listener(),
# positions_listener(),
# balance_listener(), # balance_listener(),
# return_exceptions=True return_exceptions=True
) )
except Exception as e: except Exception as e:
@@ -149,4 +186,5 @@ async def websocket_quick_test():
if __name__ == "__main__": if __name__ == "__main__":
# 运行快速测试 # 运行快速测试
# asyncio.run(quick_order_test())
asyncio.run(websocket_quick_test()) asyncio.run(websocket_quick_test())