diff --git a/.gitignore b/.gitignore index fe39a87..5fb1546 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ build/ dist/ ccxt/__pycache__/ -__pycache__/ \ No newline at end of file +__pycache__/ +.vscode/settings.json diff --git a/ccxt/async_support/mt5.py b/ccxt/async_support/mt5.py index c7e727c..578c708 100644 --- a/ccxt/async_support/mt5.py +++ b/ccxt/async_support/mt5.py @@ -52,6 +52,7 @@ class mt5(Exchange, ImplicitAPI): 'fetchOHLCV': True, 'fetchOpenOrders': True, 'fetchOrder': True, + 'fetchPositions': True, 'fetchOrderBook': True, 'fetchTicker': True, 'fetchTickers': True, @@ -561,6 +562,132 @@ class mt5(Exchange, ImplicitAPI): return tickers + async def fetch_positions(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, + } + + response = await self.private_get_openedorders(self.extend(request, params)) + + # 使用基类的 parse_positions 方法,让 parse_position 自己判断是否为有效持仓 + result = self.parse_positions(response, [symbol] if symbol else None, params) + + # 过滤掉 None 值(无效持仓) + result = [position for position in result if position is not None] + + # 应用限制 + if limit is not None: + result = result[:limit] + + return result + + def parse_position(self, position, market: Market = None): + """解析持仓信息 - 根据真实数据调整""" + # 获取市场信息 + market_id = self.safe_string(position, 'symbol') + symbol = self.safe_symbol(market_id, market, '/') + + # 检查是否为有效持仓 + state = self.safe_string(position, 'state') + lots = self.safe_number(position, 'lots', 0) + close_lots = self.safe_number(position, 'closeLots', 0) + + # 只有状态为已成交且有未平仓数量的才是有效持仓 + # 根据你的业务逻辑调整这个判断条件 + if state != 'Filled' or lots <= close_lots: + return None + + # 解析时间戳 + timestamp = self.parse8601(self.safe_string(position, 'openTime')) + open_timestamp_utc = self.safe_integer(position, 'openTimestampUTC') + 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: @@ -731,68 +858,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() diff --git a/ccxt/mt5.py b/ccxt/mt5.py index 799b9f4..dcef454 100644 --- a/ccxt/mt5.py +++ b/ccxt/mt5.py @@ -58,6 +58,7 @@ class mt5(Exchange, ImplicitAPI): 'fetchOHLCV': True, 'fetchOpenOrders': True, 'fetchOrder': True, + 'fetchPositions': True, 'fetchOrderBook': True, 'fetchTicker': True, 'fetchTickers': True, @@ -582,6 +583,132 @@ class mt5(Exchange, ImplicitAPI): 'isInvestor': self.safe_value(response, 'isInvestor', False), } + def fetch_positions(self, symbol=None, since=None, limit=None, params={}): + """异步获取持仓信息""" + if not hasattr(self, 'token') or not self.token: + self.get_token() + + request = { + 'id': self.token, + } + + response = self.private_get_openedorders(self.extend(request, params)) + + # 使用基类的 parse_positions 方法,让 parse_position 自己判断是否为有效持仓 + result = self.parse_positions(response, [symbol] if symbol else None, params) + + # 过滤掉 None 值(无效持仓) + result = [position for position in result if position is not None] + + # 应用限制 + if limit is not None: + result = result[:limit] + + return result + + def parse_position(self, position, market: Market = None): + """解析持仓信息 - 根据真实数据调整""" + # 获取市场信息 + market_id = self.safe_string(position, 'symbol') + symbol = self.safe_symbol(market_id, market, '/') + + # 检查是否为有效持仓 + state = self.safe_string(position, 'state') + lots = self.safe_number(position, 'lots', 0) + close_lots = self.safe_number(position, 'closeLots', 0) + + # 只有状态为已成交且有未平仓数量的才是有效持仓 + # 根据你的业务逻辑调整这个判断条件 + if state != 'Filled' or lots <= close_lots: + return None + + # 解析时间戳 + timestamp = self.parse8601(self.safe_string(position, 'openTime')) + open_timestamp_utc = self.safe_integer(position, 'openTimestampUTC') + 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, + } + def fetch_open_orders(self, symbol=None, since=None, limit=None, params={}): """获取未平仓订单""" self.load_token() @@ -730,64 +857,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()