Compare commits
10 Commits
4e0af9402d
...
be4c883d62
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
be4c883d62 | ||
|
|
502a0c49f2 | ||
|
|
ef0058aded | ||
|
|
2f34f56527 | ||
|
|
a630c42ddd | ||
|
|
6bb93a3968 | ||
|
|
0795f47168 | ||
|
|
efd59755d8 | ||
|
|
b298c01743 | ||
|
|
dd4d01c58a |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,4 +1,5 @@
|
|||||||
build/
|
build/
|
||||||
dist/
|
dist/
|
||||||
ccxt/__pycache__/
|
ccxt/__pycache__/
|
||||||
__pycache__/
|
__pycache__/
|
||||||
|
.vscode/settings.json
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
369
ccxt/mt5.py
369
ccxt/mt5.py
@@ -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()
|
||||||
|
|||||||
@@ -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
5072
test/data.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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())
|
||||||
Reference in New Issue
Block a user