Files
ccxt_with_mt5/ccxt/async_support/mt5.py
lz_db be4c883d62 1
2025-12-02 15:17:58 +08:00

1033 lines
39 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
from ccxt.async_support.base.exchange import Exchange
from ccxt.abstract.mt5 import ImplicitAPI
import asyncio
import hashlib
from ccxt.base.types import Any, Balances, DepositAddress, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, OrderBooks, Trade, TradingFees, Transaction
from typing import List
from typing import List, Optional # 添加 Optional 导入
from ccxt.base.errors import ExchangeError
from ccxt.base.errors import AuthenticationError
from ccxt.base.errors import ArgumentsRequired
from ccxt.base.errors import InsufficientFunds
from ccxt.base.errors import InvalidOrder
from ccxt.base.errors import OrderNotFound
from ccxt.base.errors import DDoSProtection
from ccxt.base.errors import RateLimitExceeded
from ccxt.base.errors import ExchangeNotAvailable
from ccxt.base.errors import InvalidNonce
from ccxt.base.decimal_to_precision import TICK_SIZE
from ccxt.base.precise import Precise
from datetime import datetime, timedelta
class mt5(Exchange, ImplicitAPI):
def describe(self) -> Any:
return self.deep_extend(super(mt5, self).describe(), {
'id': 'mt5',
'name': 'MT5',
'countries': ['US'],
'version': 'v2025.02.05-05.23',
'rateLimit': 1000,
'hostname': '10.203.0.6:5000',
'pro': True,
'options': {
'host': '18.163.85.196',
'port': 443,
'connectTimeoutSeconds': 30,
},
'has': {
'CORS': True,
'spot': True,
'margin': True,
'swap': False,
'future': False,
'option': False,
'cancelOrder': True,
'createOrder': True,
'fetchBalance': True,
'fetchClosedOrders': True,
'fetchMarkets': True,
'fetchMyTrades': True,
'fetchOHLCV': True,
'fetchOpenOrders': True,
'fetchOrder': True,
'fetchPositions': True,
'fetchOrderBook': True,
'fetchTicker': True,
'fetchTickers': True,
},
'timeframes': {
'1m': 1,
'5m': 5,
'15m': 15,
'30m': 30,
'1h': 60,
'4h': 240,
'1d': 1440,
'1w': 10080,
'1M': 43200,
},
'urls': {
'logo': '',
'api': {
'public': 'http://{hostname}', # 直接使用具体地址
'private': 'http://{hostname}',
},
'www': 'http://{hostname}',
'doc': ['http://{hostname}/index.html'],
},
'api': {
'public': {
'get': {
'Ping': 1
},
},
'private': {
'get': {
'Connect': 10,
'CheckConnect': 1,
'Disconnect': 1,
'Symbols': 1,
'SymbolParamsMany': 1,
'ServerTimezone':1,
'AccountSummary': 1,
'AccountDetails': 1,
'SymbolList': 1,
'GetQuote': 1,
'GetQuoteMany': 1,
'MarketWatchMany': 1,
'OpenedOrders': 1,
'ClosedOrders': 1,
'OpenedOrder': 1,
'OrderHistory': 1,
'PriceHistory': 1,
'OrderSend': 1,
'OrderModify': 1,
'OrderClose': 1,
'SubscribeOhlc': 1, # K线订阅
'UnsubscribeOhlc': 1, # K线取消订阅
'Subscribe': 1, # 行情订阅
'UnSubscribe': 1, # 行情取消订阅
},
},
},
'requiredCredentials': {
'apiKey': True,
'secret': True,
'hostname': True,
},
'commonCurrencies': {},
'exceptions': {
'exact': {
'Invalid token': AuthenticationError,
'Connection failed': ExchangeError,
'Invalid symbol': ExchangeError,
'Invalid order': InvalidOrder,
'Order not found': OrderNotFound,
},
},
})
async def get_token(self):
"""获取或刷新 token - 异步版本"""
if hasattr(self, 'token') and self.token:
try:
await self.check_connect()
return self.token
except Exception:
# Token 无效,重新连接
pass
# 重新连接获取 token
return await self.connect()
async def connect(self):
"""连接到 MT5 账户并获取 token - 异步版本"""
request = {
'user': self.apiKey,
'password': self.secret,
'host': self.options['host'],
'port': self.options['port'],
'connectTimeoutSeconds': self.options['connectTimeoutSeconds'],
}
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):
"""检查连接状态 - 异步版本"""
request = {
'id': await self.get_token(),
}
return await self.private_get_checkconnect(request)
async def disconnect(self):
"""断开连接"""
if hasattr(self, 'token') and self.token:
request = {
'id': self.token,
}
try:
await self.private_get_disconnect(request)
except Exception:
pass
finally:
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_symbolparamsmany(self.extend(request, params))
markets = []
if isinstance(response, list):
for symbol_data in response:
try:
market = self.parse_market(symbol_data)
if market and market.get('symbol'):
markets.append(market)
except Exception as e:
if self.verbose:
symbol_name = self.safe_string(symbol_data, 'symbol', 'unknown')
print(f"跳过交易对 {symbol_name}: {e}")
continue
# 设置市场数据
if markets:
self.markets = {}
self.symbols = []
for market in markets:
id = market['id']
symbol = market['symbol']
self.markets[id] = market
self.markets[symbol] = market
self.symbols.append(symbol)
self.symbols = sorted(self.symbols)
self.ids = sorted(self.markets.keys())
return markets
except Exception as e:
raise ExchangeError(f"获取市场数据失败: {e}")
def parse_market(self, symbol_data):
"""解析市场信息 - 根据新的数据格式"""
try:
if not isinstance(symbol_data, dict):
return None
symbol = self.safe_string(symbol_data, 'symbol')
if not symbol:
return None
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_name) < 6:
# 处理较短的符号
base = symbol_name
quote = 'USD' # 默认报价货币
else:
base = symbol_name[:3]
quote = symbol_name[3:]
# 解析精度信息
digits = self.safe_integer(symbol_info, 'digits', 5)
if digits is not None:
try:
digits = int(digits)
except (ValueError, TypeError):
digits = 5
# 解析合约大小
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,
'symbol': base + '/' + quote,
'base': base,
'quote': quote,
'baseId': base,
'quoteId': quote,
'active': active,
'type': 'spot',
'spot': True,
'margin': True,
'precision': {
'price': price_precision,
'amount': 2, # 手数精度
'base': 2,
'quote': price_precision,
},
'limits': {
'amount': {
'min': min_volume,
'max': max_volume,
},
'price': {
'min': points, # 最小价格变动
'max': None,
},
'cost': {
'min': None,
'max': None,
},
'leverage': {
'min': 1.0,
'max': self.safe_number(symbol_group, 'accountLeverage', 100.0),
}
},
'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:
symbol_name = self.safe_string(symbol_data, 'symbol', 'unknown')
print(f"解析市场信息失败 {symbol_name}: {e}")
return None
async def server_timezone(self):
"""获得mt5服务器时区"""
if hasattr(self, 'timezone'):
return self.timezone
else:
if not hasattr(self, 'token') or not self.token:
await self.get_token()
request = {
'id': self.token,
}
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={}):
"""获取账户余额"""
if not hasattr(self, 'token') or not self.token:
await self.get_token()
request = {
'id': self.token,
}
response = await self.private_get_accountsummary(self.extend(request, params))
return self.parse_balance(response)
def parse_balance(self, response):
"""解析余额信息"""
result = {
'info': response,
'timestamp': None,
'datetime': None,
}
currency = 'USDT'
balance = self.safe_number(response, 'balance', 0.0)
margin = self.safe_number(response, 'margin', 0.0)
free_margin = self.safe_number(response, 'freeMargin', 0.0)
equity = self.safe_number(response, 'equity', 0.0)
profit = self.safe_number(response, 'profit', 0.0)
result[currency] = {
'free': free_margin,
'used': margin,
'total': balance,
'equity': equity,
'profit': profit,
}
return self.safe_balance(result)
async def fetch_account_details(self, params={}):
"""获取账户信息"""
if not hasattr(self, 'token') or not self.token:
await self.get_token()
request = {
'id': self.token,
}
response = await self.private_get_accountdetails(self.extend(request, params))
return self.parse_account(response)
def parse_account(self, response):
"""解析账户信息"""
return {
'serverName': self.safe_string(response, 'serverName'),
'user': self.safe_string(response, 'user'),
'host': self.safe_string(response, 'host'),
'port': self.safe_integer(response, 'port'),
'serverTime': self.safe_string(response, 'serverTime'),
'serverTimeZone': self.safe_integer(response, 'serverTimeZone'),
'company': self.safe_string(response, 'company'),
'currency': self.safe_string(response, 'currency', 'UST'),
'accountName': self.safe_string(response, 'accountName'),
'group': self.safe_string(response, 'group'),
'accountType': self.safe_string(response, 'accountType'),
'accountLeverage': self.safe_integer(response, 'accountLeverage'),
'accountMethod': self.safe_string(response, 'accountMethod'),
'isInvestor': self.safe_value(response, 'isInvestor', False),
}
async def fetch_ticker(self, symbol, params={}):
"""获取行情数据"""
await self.load_markets()
market = self.market(symbol)
if not hasattr(self, 'token') or not self.token:
await self.get_token()
request = {
'id': self.token,
'symbol': market['id'],
}
response = await self.private_get_getquote(self.extend(request, params))
return self.parse_ticker(response, market)
def parse_ticker(self, ticker, market=None):
"""解析行情数据"""
symbol = market['symbol'] if market else None
timestamp = None
if ticker.get('time'):
try:
timestamp = self.parse8601(ticker.get('time'))
except:
timestamp = None
return {
'symbol': symbol,
'timestamp': timestamp,
'datetime': self.iso8601(timestamp) if timestamp else None,
'high': None,
'low': None,
'bid': self.safe_number(ticker, 'bid'),
'bidVolume': None,
'ask': self.safe_number(ticker, 'ask'),
'askVolume': None,
'vwap': None,
'open': None,
'close': None,
'last': self.safe_number(ticker, 'last'),
'previousClose': None,
'change': None,
'percentage': None,
'average': None,
'baseVolume': self.safe_number(ticker, 'volume'),
'quoteVolume': None,
'info': ticker,
}
async def fetch_tickers(self, symbols: Optional[List[str]] = None, params={}):
"""异步获取多个交易对的行情数据"""
if not hasattr(self, 'token') or not self.token:
await self.get_token()
request = {
'id': self.token,
}
# 如果指定了特定的交易对
if symbols is not None:
# 将符号列表转换为 MT5 格式
mt5_symbols = []
for symbol in symbols:
market = self.market(symbol)
mt5_symbols.append(market['id'])
request['symbols'] = mt5_symbols
try:
response = await self.private_get_getquotemany(self.extend(request, params))
return self.parse_tickers(response, symbols)
except Exception as e:
# 如果批量获取失败,回退到逐个获取
if symbols is not None:
return await self.fetch_tickers_fallback(symbols, params)
else:
raise ExchangeError(f"获取批量行情失败: {e}")
async def fetch_tickers_fallback(self, symbols, params={}):
"""异步回退方法:逐个获取交易对行情"""
tickers = {}
for symbol in symbols:
try:
ticker = await self.fetch_ticker(symbol, params)
tickers[symbol] = ticker
except Exception as e:
if self.verbose:
print(f"获取 {symbol} 行情失败: {e}")
continue
return tickers
def parse_tickers(self, response, symbols=None):
"""解析批量行情数据(与同步版本相同)"""
tickers = {}
if isinstance(response, list):
# 如果响应是数组
for ticker_data in response:
try:
ticker = self.parse_ticker(ticker_data)
if ticker and ticker.get('symbol'):
tickers[ticker['symbol']] = ticker
except Exception as e:
if self.verbose:
print(f"解析行情数据失败: {e}")
continue
elif isinstance(response, dict):
# 如果响应是字典
for symbol_key, ticker_data in response.items():
try:
ticker = self.parse_ticker(ticker_data)
if ticker and ticker.get('symbol'):
tickers[ticker['symbol']] = ticker
except Exception as e:
if self.verbose:
print(f"解析行情数据失败 {symbol_key}: {e}")
continue
# 如果指定了特定的交易对,确保返回的顺序一致
if symbols is not None:
ordered_tickers = {}
for symbol in symbols:
if symbol in tickers:
ordered_tickers[symbol] = tickers[symbol]
return ordered_tickers
return tickers
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:
await self.get_token()
request = {
'id': self.token,
}
await self.server_timezone()
response = await self.private_get_openedorders(self.extend(request, params))
# 如果指定了特定交易对,进行过滤
if symbol is not None:
market = self.market(symbol)
filtered_orders = []
for order in response:
if isinstance(order, dict) and order.get('symbol') == market['id']:
filtered_orders.append(order)
return self.parse_orders(filtered_orders, market, since, limit)
else:
return self.parse_orders(response, None, since, limit)
async def fetch_closed_orders(self, symbol=None, since=None, limit=None, params={}):
"""异步获取已平仓订单 - 修复版本"""
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_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 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(data, None, since, limit)
def parse_order(self, order, market=None):
"""解析订单信息 - 修复市场符号问题"""
try:
id = self.safe_string(order, 'ticket')
market_id = self.safe_string(order, 'symbol')
# 安全地解析市场符号
symbol = None
if market is not None:
symbol = market['symbol']
elif market_id is not None:
# 修复:提供更多参数来正确解析符号
# 假设 MT5 的符号格式是 BASEQUOTE如 EURUSD, BTCUSD
if len(market_id) >= 6:
# 尝试解析为 3+3 格式(如 EURUSD, GBPUSD
base = market_id[:3]
quote = market_id[3:]
symbol = base + '/' + quote
else:
# 如果无法解析,使用原始 market_id
symbol = market_id
timestamp = self.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'))
type = self.parse_order_type(self.safe_string(order, 'orderType'))
price = self.safe_number(order, 'openPrice')
amount = self.safe_number(order, 'lots')
filled = self.safe_number(order, 'closeLots', 0)
remaining = None
if amount is not None and filled is not None:
remaining = amount - filled
cost = None
if price is not None and filled is not None:
cost = price * filled
fee = None
fee_cost = self.safe_number(order, 'commission', 0)
if fee_cost != 0:
fee = {
'cost': fee_cost,
'currency': None,
}
# 确保所有必需字段都有值
if timestamp is None:
timestamp = self.milliseconds()
return self.safe_order({
'id': id,
'clientOrderId': self.safe_string(order, 'comment'),
'datetime': self.iso8601(timestamp),
'timestamp': timestamp,
'lastTradeTimestamp': last_trade_timestamp,
'lastUpdateTimestamp': last_trade_timestamp,
'status': status,
'symbol': symbol,
'type': type,
'timeInForce': None,
'postOnly': None,
'side': side,
'price': price,
'stopLossPrice': self.safe_number(order, 'stopLoss'),
'takeProfitPrice': self.safe_number(order, 'takeProfit'),
'reduceOnly':None,
'triggerPrice': None,
'amount': amount,
'filled': filled,
'remaining': remaining,
'cost': cost,
'trades': None,
'fee': fee,
'info': order,
'average': None,
})
except Exception as e:
if self.verbose:
print(f"解析订单失败: {e}, order: {order}")
return self.safe_order({
'id': self.safe_string(order, 'ticket'),
'symbol': symbol,
'status': 'unknown',
'side': 'unknown',
'type': 'unknown',
'timestamp': self.milliseconds(),
'info': order,
})
def parse_order_status(self, status):
statuses = {
# 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)
def parse_order_side(self, side):
sides = {
'Buy': 'buy',
'Sell': 'sell',
'BuyLimit': 'buy',
'SellLimit': 'sell',
'BuyStop': 'buy',
'SellStop': 'sell',
}
return self.safe_string(sides, side, side)
def parse_order_type(self, type):
types = {
'Buy': 'market',
'Sell': 'market',
'BuyLimit': 'limit',
'SellLimit': 'limit',
'BuyStop': 'stop',
'SellStop': 'stop',
}
return self.safe_string(types, type, type)
async def create_order(self, symbol, type, side, amount, price=None, params={}):
"""创建订单"""
await self.load_markets()
market = self.market(symbol)
if not hasattr(self, 'token') or not self.token:
await self.get_token()
request = {
'id': self.token,
'symbol': market['id'],
'volume': amount,
}
# 映射订单类型
operation_map = {
'market': {
'buy': 'Buy',
'sell': 'Sell',
},
'limit': {
'buy': 'BuyLimit',
'sell': 'SellLimit',
},
'stop': {
'buy': 'BuyStop',
'sell': 'SellStop',
},
}
if type in operation_map and side in operation_map[type]:
request['operation'] = operation_map[type][side]
else:
raise InvalidOrder(self.id + ' createOrder does not support order type ' + type + ' and side ' + side)
if type in ['limit', 'stop'] and price is not None:
request['price'] = price
# 处理止损止盈
stop_loss = self.safe_number(params, 'stopLoss')
take_profit = self.safe_number(params, 'takeProfit')
if stop_loss is not None:
request['stoploss'] = stop_loss
if take_profit is not None:
request['takeprofit'] = take_profit
response = await self.private_get_ordersend(self.extend(request, params))
return self.parse_order(response, market)
async def cancel_order(self, id, symbol=None, params={}):
"""取消订单"""
if not hasattr(self, 'token') or not self.token:
await self.get_token()
request = {
'id': self.token,
'ticket': int(id),
}
response = await self.private_get_orderclose(self.extend(request, params))
return self.parse_order(response)
async def private_get(self, endpoint, params={}):
"""发送私有 GET 请求"""
return await self.fetch_private(endpoint, 'GET', params)
async def fetch_private(self, path, method='GET', params={}, headers=None, body=None):
"""发送私有 API 请求"""
base_url = self.implode_hostname(self.urls['api']['private'])
url = base_url + '/' + path
query = self.omit(params, self.extract_params(path))
if method == 'GET' and query:
url += '?' + self.urlencode(query)
if self.verbose:
print(f"🔧 发送请求: {url}")
import aiohttp
try:
async with aiohttp.ClientSession() as session:
async with session.get(url, timeout=aiohttp.ClientTimeout(total=30)) as response:
if response.status == 200:
content = await response.text()
return self.parse_json(content)
else:
error_text = await response.text()
raise ExchangeError(f"HTTP {response.status}: {error_text}")
except asyncio.TimeoutError:
raise ExchangeError("请求超时")
except Exception as e:
raise ExchangeError(f"请求失败: {e}")
def parse_json(self, response):
"""解析响应,支持多种格式"""
if not response:
return response
response = response.strip()
# 处理常见的成功响应
if response in ['OK', 'SUCCESS', 'True', 'true']:
return True
# 处理常见的失败响应
if response in ['FAIL', 'ERROR', 'False', 'false']:
return False
# 尝试解析 JSON
try:
import json
return json.loads(response)
except json.JSONDecodeError:
# 不是 JSON返回原始响应
return response
except Exception as e:
if self.verbose:
print(f"响应解析失败: {e}, 响应: {response}")
return response
def sign(self, path, api='public', method='GET', params={}, headers=None, body=None):
"""签名请求"""
base_url = self.implode_hostname(self.urls['api'][api])
url = base_url + '/' + path
query = self.omit(params, self.extract_params(path))
if method == 'GET' and query:
url += '?' + self.urlencode(query)
# 调试信息
if self.verbose:
print(f"🔧 Debug: Final URL: {url}")
print(f"🔧 Debug: Method: {method}")
print(f"🔧 Debug: Params: {params}")
return {
'url': url,
'method': method,
'body': body,
'headers': headers
}