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

1
.gitignore vendored
View File

@@ -2,3 +2,4 @@ build/
dist/
ccxt/__pycache__/
__pycache__/
.vscode/settings.json

View File

@@ -1,4 +1,4 @@
Metadata-Version: 2.4
Metadata-Version: 2.1
Name: ccxt
Version: 4.5.18
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: Environment :: Console
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
Requires-Dist: ruff==0.0.292; extra == "qa"
Requires-Dist: tox>=4.8.0; extra == "qa"
Provides-Extra: type
Requires-Dist: mypy==1.6.1; extra == "type"
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
License-File: LICENSE.txt
# 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.decimal_to_precision import TICK_SIZE
from ccxt.base.precise import Precise
from datetime import datetime, timedelta
class mt5(Exchange, ImplicitAPI):
@@ -52,6 +53,7 @@ class mt5(Exchange, ImplicitAPI):
'fetchOHLCV': True,
'fetchOpenOrders': True,
'fetchOrder': True,
'fetchPositions': True,
'fetchOrderBook': True,
'fetchTicker': True,
'fetchTickers': True,
@@ -154,6 +156,10 @@ class mt5(Exchange, ImplicitAPI):
response = await self.private_get_connect(request)
self.token = response
self.diff_milliseconds = 0 # 重置时间差
# 获取服务器时区
await self.server_timezone()
return self.token
async def check_connect(self):
@@ -177,28 +183,29 @@ class mt5(Exchange, ImplicitAPI):
self.token = None
async def fetch_markets(self, params={}):
"""获取交易对列表 - 异步修复版本"""
"""获取交易对列表 - 使用 SymbolParamsMany 接口"""
if not hasattr(self, 'token') or not self.token:
await self.get_token()
request = {
'id': self.token,
'limit': 10000, # 添加 limit 参数获取所有交易对
}
try:
response = await self.private_get_symbols(self.extend(request, params))
response = await self.private_get_symbolparamsmany(self.extend(request, params))
markets = []
if isinstance(response, dict):
for symbol, info in response.items():
if isinstance(response, list):
for symbol_data in response:
try:
market = self.parse_market(info)
market = self.parse_market(symbol_data)
if market and market.get('symbol'):
markets.append(market)
except Exception as e:
# 跳过解析失败的市场,继续处理其他市场
if self.verbose:
print(f"跳过交易对 {symbol}: {e}")
symbol_name = self.safe_string(symbol_data, 'symbol', 'unknown')
print(f"跳过交易对 {symbol_name}: {e}")
continue
# 设置市场数据
@@ -220,42 +227,72 @@ class mt5(Exchange, ImplicitAPI):
except Exception as e:
raise ExchangeError(f"获取市场数据失败: {e}")
def parse_market(self, info):
"""解析市场信息 - 更健壮的版本"""
def parse_market(self, symbol_data):
"""解析市场信息 - 根据新的数据格式"""
try:
# 安全获取 symbol
if not isinstance(info, dict):
if not isinstance(symbol_data, dict):
return None
symbol = self.safe_string(info, 'currency', '')
symbol = self.safe_string(symbol_data, 'symbol')
if not symbol:
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)
if len(symbol) < 6:
if len(symbol_name) < 6:
# 处理较短的符号
base = symbol
base = symbol_name
quote = 'USD' # 默认报价货币
else:
base = symbol[:3]
quote = symbol[3:]
base = symbol_name[:3]
quote = symbol_name[3:]
base = symbol[:3]
quote = symbol[3:]
# 安全处理精度
digits = self.safe_integer(info, 'digits', 5)
# 确保 digits 是整数
# 解析精度信息
digits = self.safe_integer(symbol_info, 'digits', 5)
if digits is not None:
try:
digits = int(digits)
except (ValueError, TypeError):
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 {
'id': market_id,
@@ -264,33 +301,61 @@ class mt5(Exchange, ImplicitAPI):
'quote': quote,
'baseId': base,
'quoteId': quote,
'active': True,
'active': active,
'type': 'spot',
'spot': True,
'margin': True,
'precision': {
'price': digits,
'amount': 2,
'price': price_precision,
'amount': 2, # 手数精度
'base': 2,
'quote': price_precision,
},
'limits': {
'amount': {
'min': self.safe_number(info, 'minVolume', 0.01),
'max': self.safe_number(info, 'maxVolume'),
'min': min_volume,
'max': max_volume,
},
'price': {
'min': None,
'min': points, # 最小价格变动
'max': None,
},
'cost': {
'min': 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:
if self.verbose:
print(f"解析市场信息失败: {e}, info: {info}")
symbol_name = self.safe_string(symbol_data, 'symbol', 'unknown')
print(f"解析市场信息失败 {symbol_name}: {e}")
return None
async def server_timezone(self):
@@ -306,6 +371,7 @@ class mt5(Exchange, ImplicitAPI):
}
response = await self.private_get_servertimezone(request)
self.timezone = int(float(response))
self.diff_milliseconds = self.timezone * 3600 * 1000
return self.timezone
async def fetch_balance(self, params={}):
@@ -502,6 +568,131 @@ class mt5(Exchange, ImplicitAPI):
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={}):
"""异步获取未平仓订单 - 修复版本"""
if not hasattr(self, 'token') or not self.token:
@@ -511,6 +702,8 @@ class mt5(Exchange, ImplicitAPI):
'id': self.token,
}
await self.server_timezone()
response = await self.private_get_openedorders(self.extend(request, params))
# 如果指定了特定交易对,进行过滤
@@ -529,22 +722,34 @@ class mt5(Exchange, ImplicitAPI):
if not hasattr(self, 'token') or not self.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 = {
'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:
market = self.market(symbol)
filtered_orders = []
for order in response:
for order in data:
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)
return self.parse_orders(data, None, since, limit)
def parse_order(self, order, market=None):
"""解析订单信息 - 修复市场符号问题"""
@@ -568,8 +773,13 @@ class mt5(Exchange, ImplicitAPI):
# 如果无法解析,使用原始 market_id
symbol = market_id
timestamp = self.parse8601(self.safe_string(order, 'openTime'))
last_trade_timestamp = self.parse8601(self.safe_string(order, 'closeTime'))
timestamp = self.safe_integer(order, 'openTimestampUTC') - self.diff_milliseconds
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'))
side = self.parse_order_side(self.safe_string(order, 'orderType'))
@@ -600,7 +810,7 @@ class mt5(Exchange, ImplicitAPI):
return self.safe_order({
'id': id,
'clientOrderId': None,
'clientOrderId': self.safe_string(order, 'comment'),
'datetime': self.iso8601(timestamp),
'timestamp': timestamp,
'lastTradeTimestamp': last_trade_timestamp,
@@ -640,13 +850,22 @@ class mt5(Exchange, ImplicitAPI):
def parse_order_status(self, status):
statuses = {
'Started': 'open',
'Placed': 'open',
'Cancelled': 'canceled',
'Partial': 'open',
'Filled': 'closed',
'Rejected': 'rejected',
'Expired': 'expired',
# MT5 状态 -> CCXT 标准状态
'Started': 'open', # 订单已开始
'Placed': 'open', # 订单已放置
'RequestAdding': 'pending', # 请求添加订单(待处理)
'RequestModifying': 'pending', # 请求修改订单(待处理)
'RequestCancelling': 'pending', # 请求取消订单(待处理)
'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)
@@ -672,68 +891,6 @@ class mt5(Exchange, ImplicitAPI):
}
return self.safe_string(types, type, type)
def parse_position(self, order_data, market: Market = None):
"""从订单数据解析持仓"""
# 只有状态为 Filled 的订单才是持仓
state = self.safe_string(order_data, 'state')
if state != 'Filled':
return None
symbol = self.safe_string(order_data, 'symbol')
if symbol and len(symbol) >= 6:
base = symbol[:3]
quote = symbol[3:]
symbol = base + '/' + quote
timestamp = self.parse8601(self.safe_string(order_data, 'openTime'))
open_timestamp_utc = self.safe_integer(order_data, 'openTimestampUTC')
if open_timestamp_utc:
timestamp = open_timestamp_utc
# 确定持仓方向
order_type = self.safe_string(order_data, 'orderType')
side = 'long' if order_type == 'Buy' else 'short'
# 计算持仓价值
contracts = self.safe_number(order_data, 'lots', 0)
entry_price = self.safe_number(order_data, 'openPrice', 0)
current_price = self.safe_number(order_data, 'closePrice', entry_price)
notional = contracts * entry_price if contracts and entry_price else None
# 计算盈亏百分比
percentage = None
if entry_price and current_price and entry_price != 0:
if side == 'long':
percentage = (current_price - entry_price) / entry_price
else:
percentage = (entry_price - current_price) / entry_price
return {
'id': self.safe_string(order_data, 'ticket'),
'symbol': symbol,
'timestamp': timestamp,
'datetime': self.iso8601(timestamp),
'side': side,
'contracts': contracts,
'contractSize': self.safe_number(order_data, 'contractSize', 1.0),
'entryPrice': entry_price,
'markPrice': current_price, # 使用当前价格作为标记价格
'notional': notional,
'leverage': 1, # MT5 可能需要从账户信息获取
'unrealizedPnl': self.safe_number(order_data, 'profit', 0),
'realizedPnl': 0, # 对于持仓已实现盈亏为0
'liquidationPrice': None, # MT5 可能不提供
'marginMode': 'cross',
'percentage': percentage,
'marginRatio': None,
'collateral': None,
'initialMargin': None, # 可能需要计算
'initialMarginPercentage': None,
'maintenanceMargin': None,
'maintenanceMarginPercentage': None,
# 'info': order_data,
}
async def create_order(self, symbol, type, side, amount, price=None, params={}):
"""创建订单"""
await self.load_markets()

View File

@@ -24,6 +24,7 @@ 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
from datetime import datetime, timedelta
class mt5(Exchange, ImplicitAPI):
@@ -58,6 +59,7 @@ class mt5(Exchange, ImplicitAPI):
'fetchOHLCV': True,
'fetchOpenOrders': True,
'fetchOrder': True,
'fetchPositions': True,
'fetchOrderBook': True,
'fetchTicker': True,
'fetchTickers': True,
@@ -168,6 +170,8 @@ class mt5(Exchange, ImplicitAPI):
response = self.private_get_connect(params)
self.token = response
self.diff_milliseconds = 0 # 服务器时区差异,默认设置为 0
self.server_timezone()
return self.token
def check_connect(self):
@@ -206,28 +210,31 @@ class mt5(Exchange, ImplicitAPI):
}
response = self.private_get_servertimezone(request)
self.timezone = int(float(response))
self.diff_milliseconds = self.timezone * 3600 * 1000
return self.timezone
def fetch_markets(self, params={}):
"""获取交易对列表 - 修复版本"""
"""获取交易对列表 - 使用 SymbolParamsMany 接口"""
self.load_token()
request = {
'id': self.token,
'limit': 10000, # 添加 limit 参数获取所有交易对
}
try:
response = self.private_get_symbols(self.extend(request, params))
response = self.private_get_symbolparamsmany(self.extend(request, params))
markets = []
if isinstance(response, dict):
for symbol, info in response.items():
if isinstance(response, list):
for symbol_data in response:
try:
market = self.parse_market(info)
market = self.parse_market(symbol_data)
if market and market.get('symbol'):
markets.append(market)
except Exception as e:
if self.verbose:
print(f"跳过交易对 {symbol}: {e}")
symbol_name = self.safe_string(symbol_data, 'symbol', 'unknown')
print(f"跳过交易对 {symbol_name}: {e}")
continue
# 设置市场数据
@@ -249,34 +256,72 @@ class mt5(Exchange, ImplicitAPI):
except Exception as e:
raise ExchangeError(f"获取市场数据失败: {e}")
def parse_market(self, info):
"""解析市场信息 - 更健壮的版本"""
def parse_market(self, symbol_data):
"""解析市场信息 - 根据新的数据格式"""
try:
if not isinstance(info, dict):
if not isinstance(symbol_data, dict):
return None
symbol = self.safe_string(info, 'currency', '')
symbol = self.safe_string(symbol_data, 'symbol')
if not symbol:
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:
base = symbol
quote = 'USD'
# 解析基础信息
symbol_name = symbol.upper().strip()
# 确保符号格式正确 (如 EURUSD)
if len(symbol_name) < 6:
# 处理较短的符号
base = symbol_name
quote = 'USD' # 默认报价货币
else:
base = symbol[:3]
quote = symbol[3:]
base = symbol_name[: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:
try:
digits = int(digits)
except (ValueError, TypeError):
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 {
'id': market_id,
@@ -285,33 +330,61 @@ class mt5(Exchange, ImplicitAPI):
'quote': quote,
'baseId': base,
'quoteId': quote,
'active': True,
'active': active,
'type': 'spot',
'spot': True,
'margin': True,
'precision': {
'price': digits,
'amount': 2,
'price': price_precision,
'amount': 2, # 手数精度
'base': 2,
'quote': price_precision,
},
'limits': {
'amount': {
'min': self.safe_number(info, 'minVolume', 0.01),
'max': self.safe_number(info, 'maxVolume'),
'min': min_volume,
'max': max_volume,
},
'price': {
'min': None,
'min': points, # 最小价格变动
'max': None,
},
'cost': {
'min': 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:
if self.verbose:
print(f"解析市场信息失败: {e}, info: {info}")
symbol_name = self.safe_string(symbol_data, 'symbol', 'unknown')
print(f"解析市场信息失败 {symbol_name}: {e}")
return None
def fetch_ticker(self, symbol, params={}):
@@ -514,9 +587,138 @@ class mt5(Exchange, ImplicitAPI):
'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={}):
"""获取未平仓订单"""
self.load_token()
self.server_timezone()
request = {
'id': self.token,
}
@@ -536,21 +738,32 @@ class mt5(Exchange, ImplicitAPI):
def fetch_closed_orders(self, symbol=None, since=None, limit=None, params={}):
"""获取已平仓订单"""
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 = {
'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:
market = self.market(symbol)
filtered_orders = []
for order in response:
for order in data:
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)
return self.parse_orders(data, None, since, limit)
def parse_order(self, order, market=None):
"""解析订单信息"""
@@ -569,8 +782,13 @@ class mt5(Exchange, ImplicitAPI):
else:
symbol = market_id
timestamp = self.parse8601(self.safe_string(order, 'openTime'))
last_trade_timestamp = self.parse8601(self.safe_string(order, 'closeTime'))
timestamp = self.safe_integer(order, 'openTimestampUTC') - self.diff_milliseconds
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'))
side = self.parse_order_side(self.safe_string(order, 'orderType'))
@@ -598,7 +816,7 @@ class mt5(Exchange, ImplicitAPI):
return self.safe_order({
'id': id,
'clientOrderId': None,
'clientOrderId': self.safe_string(order, 'comment'),
'datetime': self.iso8601(timestamp),
'timestamp': timestamp,
'lastTradeTimestamp': last_trade_timestamp,
@@ -630,13 +848,22 @@ class mt5(Exchange, ImplicitAPI):
def parse_order_status(self, status):
statuses = {
'Started': 'open',
'Placed': 'open',
'Cancelled': 'canceled',
'Partial': 'open',
'Filled': 'closed',
'Rejected': 'rejected',
'Expired': 'expired',
# MT5 状态 -> CCXT 标准状态
'Started': 'open', # 订单已开始
'Placed': 'open', # 订单已放置
'RequestAdding': 'pending', # 请求添加订单(待处理)
'RequestModifying': 'pending', # 请求修改订单(待处理)
'RequestCancelling': 'pending', # 请求取消订单(待处理)
'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)
@@ -662,64 +889,6 @@ class mt5(Exchange, ImplicitAPI):
}
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={}):
"""创建订单"""
self.load_token()

View File

@@ -67,6 +67,7 @@ class mt5(mt5Parent):
"""监听订单变化"""
if not hasattr(self, 'token') or not self.token:
await self.get_token()
await self.server_timezone()
ws_url = self.implode_hostname(self.urls['api']['ws'])
url = ws_url + '/OnOrderUpdate?id=' + self.token
message_hash = 'orders'
@@ -92,6 +93,7 @@ class mt5(mt5Parent):
"""监听持仓变化"""
if not hasattr(self, 'token') or not self.token:
await self.get_token()
await self.server_timezone()
ws_url = self.implode_hostname(self.urls['api']['ws'])
url = ws_url + '/OnOrderUpdate?id=' + self.token
message_hash = 'positions'
@@ -365,7 +367,7 @@ class mt5(mt5Parent):
"""处理 OrderUpdate 类型消息(包含订单、持仓、余额)"""
try:
data = self.safe_value(message, 'data', {})
timestamp = self.safe_integer(message, 'timestampUTC')
timestamp = self.safe_integer(message, 'timestampUTC') - self.diff_milliseconds
# 1. 解析余额信息
balance_data = self.parse_ws_balance_from_data(data)
@@ -382,6 +384,7 @@ class mt5(mt5Parent):
update_data = self.safe_value(data, 'update', {})
order_data = self.safe_value(update_data, 'order')
if order_data:
order_data['update_type'] = self.safe_value(update_data, 'type', None) # 这个字段可以判断是开仓还是平仓
order = self.parse_ws_order(order_data)
if order:
# 使用简单的列表而不是 ArrayCacheBySymbolById
@@ -426,7 +429,7 @@ class mt5(mt5Parent):
"""处理 OpenedOrders 类型消息(只包含持仓)"""
try:
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)
@@ -601,7 +604,7 @@ class mt5(mt5Parent):
"""解析单个订单数据"""
if not order_data:
return None
# print("++++++",order_data)
try:
symbol = self.safe_string(order_data, 'symbol')
if symbol and len(symbol) >= 6:
@@ -617,21 +620,37 @@ class mt5(mt5Parent):
close_time = self.safe_string(order_data, 'closeTime')
is_closed = close_time and close_time != "0001-01-01T00:00:00"
timestamp = self.parse8601(self.safe_string(order_data, 'openTime'))
if timestamp is None:
timestamp = self.milliseconds()
timestamp = self.safe_integer(order_data, 'openTimestampUTC') - self.diff_milliseconds
last_trade_timestamp = self.safe_integer(order_data, 'closeTimestampUTC')
last_trade_timestamp = None
if is_closed:
last_trade_timestamp = self.parse8601(close_time)
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
mt5_order_type = self.safe_string(order_data, 'update_type', None)
amount = self.safe_number(order_data, 'lots', 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
return {
'id': self.safe_string(order_data, 'ticket'),
'clientOrderId': None,
'clientOrderId': self.safe_string(order_data, 'comment'),
'datetime': self.iso8601(timestamp),
'timestamp': timestamp,
'lastTradeTimestamp': last_trade_timestamp,
@@ -641,8 +660,8 @@ class mt5(mt5Parent):
'type': self.parse_order_type(self.safe_string(order_data, 'orderType')),
'timeInForce': None,
'postOnly': None,
'side': self.parse_order_side(self.safe_string(order_data, 'orderType')),
'price': self.safe_number(order_data, 'openPrice'),
'side': side,
'price': price,
'stopLossPrice': self.safe_number(order_data, 'stopLoss'),
'takeProfitPrice': self.safe_number(order_data, 'takeProfit'),
'reduceOnly': None,
@@ -698,19 +717,27 @@ class mt5(mt5Parent):
quote = symbol[3:]
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')
entry_price = self.safe_number(order_data, 'openPrice')
mark_price = self.safe_number(order_data, 'closePrice')
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 {
'id': self.safe_string(order_data, 'ticket'),
'symbol': symbol,
'timestamp': timestamp,
'datetime': self.iso8601(timestamp),
'side': self.parse_order_side(self.safe_string(order_data, 'orderType')),
'side': side,
'contracts': contracts,
'contractSize': self.safe_number(order_data, 'contractSize', 1.0),
'entryPrice': entry_price,
@@ -728,9 +755,14 @@ class mt5(mt5Parent):
'initialMarginPercentage': None,
'maintenanceMargin': None,
'maintenanceMarginPercentage': None,
'stopLossPrice': stop_loss_price,
'takeProfitPrice': take_profit_price,
'lastPrice': mark_price,
'hedged': None,
'info': order_data,
}
def parse_ws_ohlcv_message(self, data):
"""解析WebSocket K线数据"""
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("🚀 快速订单测试开始")
exchange = mt5({
'user': 62333850,
'password': 'tecimil4',
'host': '78.140.180.198',
'port': 443,
'sandbox': True,
'apiKey': '76888962',
'secret': 'LZ-trade666888',
'verbose': False, # 启用详细日志
'hostname': '43.133.21.145:5000',
'options': {
# 'server': '147.160.254.81:443', # 使用服务器名称
# 或者
'host': '18.163.85.196',
'port': 443,
},
})
try:
# 测试连接
balance = await exchange.fetch_balance()
logger.info(f"✅ 连接成功,余额: {balance['total'].get('USD', 'N/A')}")
# balance = await exchange.fetch_balance()
# logger.info(f"✅ 连接成功,余额: {balance}")
positions = await exchange.fetch_positions()
logger.info(f"✅ 连接成功,信息: {positions}")
# 获取市场信息
markets = await exchange.fetch_markets()
logger.info(f"✅ 获取到 {len(markets)} 个交易对")
# markets = await exchange.fetch_markets()
# logger.info(f"✅ 获取到 {len(markets)} 个交易对")
# 获取当前价格
ticker = await exchange.fetch_ticker('EUR/USD')
logger.info(f"✅ EUR/USD 当前价格: 买={ticker['bid']}, 卖={ticker['ask']}")
# ticker = await exchange.fetch_ticker('EUR/USD')
# logger.info(f"✅ EUR/USD 当前价格: 买={ticker['bid']}, 卖={ticker['ask']}")
# 获取订单簿
orderbook = await exchange.fetch_order_book('EUR/USD')
logger.info(f"✅ EUR/USD 订单簿深度: {len(orderbook['bids'])} 买单, {len(orderbook['asks'])} 卖单")
# orderbook = await exchange.fetch_order_book('EUR/USD')
# logger.info(f"✅ EUR/USD 订单簿深度: {len(orderbook['bids'])} 买单, {len(orderbook['asks'])} 卖单")
# 获取开单
open_orders = await exchange.fetch_open_orders()
logger.info(f"✅ 当前开单数量: {len(open_orders)}")
# open_orders = await exchange.fetch_open_orders()
# logger.info(f"✅ 当前开单数量: {len(open_orders)}")
for order in open_orders:
logger.info(f" 订单 {order['id']}: {order['symbol']} {order['side']} {order['type']} {order['status']}")
# 获取订单
# 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:
# logger.info(f" 订单 {order['id']}: {order['symbol']} {order['side']} {order['type']} {order['status']}")
except Exception as e:
logger.error(f"❌ 测试失败: {e}")
# 抛出异常,终止程序
raise e
finally:
await exchange.close()
@@ -107,37 +124,57 @@ async def websocket_quick_test():
'apiKey': '76888962',
'secret': 'LZ-trade666888',
'verbose': False, # 启用详细日志
# 'debug': True, # 启用详细调试信息
'hostname': '43.167.188.220:5000',
'host': '18.163.85.196',
'port': 443,
'hostname': '43.133.21.145:5000',
'options': {
# 'server': '147.160.254.81:443', # 使用服务器名称
# 或者
'host': '18.163.85.196',
'port': 443,
},
})
try:
# 监听all
async def all_listener():
while True:
print("111111111")
res = await exchange.watch_all()
print("==========================收到信息")
print(f"收到数据:{res}")
# 监听订单更新
async def order_listener():
while True:
# print("111111111")
res = await exchange.watch_ticker(symbol='BTCUSD')
logger.error("aaa")
logger.warning("bbb")
logger.debug("ccc")
print("111111111")
res = await exchange.watch_orders()
print("===========================收到信息")
print(res)
# for order in res:
# logger.info(f"📦 订单更新: {order}")
for order in res:
# del order['info']
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():
balance = await exchange.watch_balance()
total = sum([v for v in balance['total'].values() if v is not None])
logger.info(f"💰 余额更新: 总余额 {total:.2f}")
# async def balance_listener():
# balance = await exchange.watch_balance()
# total = sum([v for v in balance['total'].values() if v is not None])
# logger.info(f"💰 余额更新: 总余额 {total:.2f}")
# 运行监听器
await asyncio.gather(
# all_listener(),
order_listener(),
# positions_listener(),
# balance_listener(),
# return_exceptions=True
return_exceptions=True
)
except Exception as e:
@@ -149,4 +186,5 @@ async def websocket_quick_test():
if __name__ == "__main__":
# 运行快速测试
# asyncio.run(quick_order_test())
asyncio.run(websocket_quick_test())