diff --git a/backend/app/crypto_agent/crypto_agent.py b/backend/app/crypto_agent/crypto_agent.py index fca43f5..2138a50 100644 --- a/backend/app/crypto_agent/crypto_agent.py +++ b/backend/app/crypto_agent/crypto_agent.py @@ -2168,6 +2168,7 @@ class CryptoAgent: async def _execute_bitget_close(self, decision: Dict[str, Any], current_price: float) -> Dict[str, Any]: """执行 Bitget 市价平仓""" + import math try: symbol = decision.get('symbol', '').replace('USDT', '') @@ -2181,20 +2182,37 @@ class CryptoAgent: if not position: return {"success": False, "error": "未找到持仓"} - size_in_coins = abs(position["size"]) + size_in_coins = abs(position["size"]) # 已经是 BTC 数量 is_long = position["size"] > 0 - contracts = self.bitget.coins_to_contracts(symbol, size_in_coins) - if contracts < 1: - return {"success": False, "error": f"持仓过小,无法下单({size_in_coins} 币 = {contracts} 张)"} + # 精度处理:向下取整到 0.0001(Bitget 最小精度) + size_in_coins = math.floor(size_in_coins * 10000) / 10000 - result = self.bitget.place_market_order( - symbol=symbol, - is_buy=not is_long, - size=contracts, - reduce_only=True - ) - return result + if size_in_coins < 0.0001: + return {"success": False, "error": f"持仓过小({size_in_coins} 币 < 最小 0.0001)"} + + # 直接使用 BTC 数量平仓,不经过合约转换 + try: + ccxt_symbol = self.bitget.trading_api._standardize_symbol(symbol + 'USDT') + side = 'sell' if is_long else 'buy' + order = self.bitget.trading_api.exchange.create_market_order( + symbol=ccxt_symbol, + side=side, + amount=size_in_coins, + params={ + 'reduceOnly': True, + 'tdMode': 'cross', + 'marginCoin': 'USDT', + } + ) + if order: + logger.info(f"✅ Bitget 平仓成功: {symbol} {side} {size_in_coins} BTC") + return {"success": True, "order_id": str(order.get('id', '')), "symbol": symbol, "size": size_in_coins} + else: + return {"success": False, "error": "下单返回空"} + except Exception as e: + logger.error(f"❌ Bitget 平仓下单失败: {e}") + return {"success": False, "error": str(e)} except Exception as e: logger.error(f"Bitget 平仓失败: {e}") diff --git a/backend/app/services/bitget_live_trading_service.py b/backend/app/services/bitget_live_trading_service.py index 4140c16..9c5a234 100644 --- a/backend/app/services/bitget_live_trading_service.py +++ b/backend/app/services/bitget_live_trading_service.py @@ -155,15 +155,18 @@ class BitgetLiveTradingService: raw_positions = self.trading_api.get_position() result = [] for pos in raw_positions: - contracts = float(pos.get('contracts', 0)) - if contracts == 0: + # 使用 info.available 获取实际持仓量(币数量) + # 注意:CCXT 的 'contracts' 字段对于 Bitget 实际上已经是币数量,不是张数 + info = pos.get('info', {}) + available = float(info.get('available', 0)) + if available == 0: continue symbol_raw = pos.get('symbol', '') # e.g. "BTC/USDT:USDT" - coin = symbol_raw.split('/')[0] if '/' in symbol_raw else symbol_raw + coin = symbol_raw.split('/')[0] if '/' in symbol_raw else symbol_raw.replace('/USDT:USDT', '') - contract_size = self.get_contract_size(coin) - coin_amount = contracts * contract_size + # available 已经是币数量,不需要再乘以 contract_size + coin_amount = available side = pos.get('side', 'long') size = coin_amount if side == 'long' else -coin_amount @@ -490,16 +493,44 @@ class BitgetLiveTradingService: def market_close_all(self) -> Dict[str, Any]: """市价平仓所有持仓""" + import math results = [] positions = self.get_open_positions() for pos in positions: coin = pos['coin'] is_long = pos['size'] > 0 - contracts = self.coins_to_contracts(coin, abs(pos['size'])) - if contracts < 1: + coin_amount = abs(pos['size']) + + # 精度处理:向下取整到 0.0001(Bitget 最小精度) + coin_amount = math.floor(coin_amount * 10000) / 10000 + + if coin_amount < 0.0001: + logger.warning(f"{coin} 持仓过小 ({coin_amount}),跳过") continue - result = self.place_market_order(coin, is_buy=not is_long, size=contracts, reduce_only=True) - results.append(result) + + # 直接使用币数量平仓,不经过合约转换 + try: + ccxt_symbol = self.trading_api._standardize_symbol(coin + 'USDT') + side = 'sell' if is_long else 'buy' + order = self.trading_api.exchange.create_market_order( + symbol=ccxt_symbol, + side=side, + amount=coin_amount, + params={ + 'reduceOnly': True, + 'tdMode': 'cross', + 'marginCoin': 'USDT', + } + ) + if order: + logger.info(f"✅ Bitget 平仓成功: {coin} {side} {coin_amount}") + results.append({"success": True, "coin": coin, "size": coin_amount}) + else: + results.append({"success": False, "coin": coin, "error": "返回空"}) + except Exception as e: + logger.error(f"❌ Bitget 平仓失败: {coin} {e}") + results.append({"success": False, "coin": coin, "error": str(e)}) + all_ok = all(r.get('success') for r in results) return {"success": all_ok, "results": results} diff --git a/backend/app/services/bitget_trading_api_sdk.py b/backend/app/services/bitget_trading_api_sdk.py index 05949af..e7a41be 100644 --- a/backend/app/services/bitget_trading_api_sdk.py +++ b/backend/app/services/bitget_trading_api_sdk.py @@ -253,7 +253,7 @@ class BitgetTradingAPI: - None: 自动判断(查询持仓后决定) - 'buy': 平空仓 - 'sell': 平多仓 - size: 平仓数量(不传则全部平仓) + size: 平仓数量(不传则全部平仓)- 传张数 price: 平仓价格(不传则市价) Returns: @@ -271,8 +271,9 @@ class BitgetTradingAPI: # 查找有持仓的仓位 position = None for pos in positions: - contracts = float(pos.get('contracts', 0)) - if contracts != 0: + # 使用 info.available 字段获取实际持仓量(BTC单位) + available = float(pos.get('info', {}).get('available', 0)) + if available != 0: position = pos break @@ -280,7 +281,8 @@ class BitgetTradingAPI: logger.warning(f"{symbol} 持仓数量为 0,无需平仓") return None - current_size = abs(float(position.get('contracts', 0))) + # 获取实际持仓量(BTC单位) + current_size_btc = abs(float(position.get('info', {}).get('available', 0))) pos_side = position.get('side') # 'long' or 'short' # 如果没有指定平仓方向,根据持仓方向自动判断 @@ -289,22 +291,45 @@ class BitgetTradingAPI: side = 'sell' if pos_side == 'long' else 'buy' # 如果没有指定平仓数量,则全部平仓 - close_size = size if size else current_size + # 注意:直接使用 BTC 数量,不再通过 place_order 转换 + close_size_btc = size * self._get_contract_size(symbol) if size else current_size_btc - logger.info(f"平仓: {symbol} 持仓方向={pos_side}, 平仓方向={side}, 数量={close_size}") + # 精度处理:向下取整到 0.0001 BTC(Bitget 最小精度) + import math + close_size_btc = math.floor(close_size_btc * 10000) / 10000 - # 执行平仓 - order_type = 'limit' if price else 'market' - order = self.place_order( - symbol=symbol, - side=side, - order_type=order_type, - size=close_size, - price=price - ) + if close_size_btc < 0.0001: + logger.warning(f"{symbol} 平仓数量 {close_size_btc} BTC 小于最小交易单位 0.0001 BTC") + return None + + logger.info(f"平仓: {symbol} 持仓方向={pos_side}, 平仓方向={side}, 数量={close_size_btc} BTC") + + # 直接使用 CCXT 下市价平仓单(绕过 place_order 的张数转换) + order_type = 'market' if not price else 'limit' + params = { + 'reduceOnly': True, + 'tdMode': 'cross', + 'marginCoin': 'USDT', + } + + if price: + order = self.exchange.create_limit_order( + symbol=ccxt_symbol, + side=side, + amount=close_size_btc, + price=price, + params=params + ) + else: + order = self.exchange.create_market_order( + symbol=ccxt_symbol, + side=side, + amount=close_size_btc, + params=params + ) if order: - logger.info(f"✅ 平仓成功: {symbol} {side} {close_size}张") + logger.info(f"✅ 平仓成功: {symbol} {side} {close_size_btc} BTC") return order return None @@ -404,7 +429,7 @@ class BitgetTradingAPI: pos_side = position.get('side') mark_price = float(position.get('markPrice', 0)) - logger.info(f"当前持仓: {symbol} {pos_side} {contracts}张, 标记价={mark_price}") + logger.info(f"当前持仓: {symbol} {pos_side} contracts={contracts}, 标记价={mark_price}") # 验证价格 if pos_side == 'long': @@ -425,26 +450,13 @@ class BitgetTradingAPI: # 使用独立的止损/止盈计划订单 # 注意:这种方式需要在平仓时也取消这些计划订单 - # 获取合约规格(用于转换张数为币数量) - if 'BTC' in symbol: - contract_size = 0.01 - elif 'ETH' in symbol: - contract_size = 0.1 - elif 'SOL' in symbol: - contract_size = 1 - elif 'BNB' in symbol: - contract_size = 0.1 - elif 'XRP' in symbol: - contract_size = 10 - elif 'DOGE' in symbol: - contract_size = 100 - elif 'MATIC' in symbol or 'POL' in symbol: - contract_size = 10 - else: - contract_size = 1 + # CCXT 的 contracts 字段对于 Bitget 实际上已经是 BTC 数量 + # 所以我们直接使用,不需要再乘以 contract_size + btc_amount = abs(contracts) - # 将张数转换为币数量 - contracts_amount = contracts * contract_size + # 精度处理 + import math + btc_amount = math.floor(btc_amount * 10000) / 10000 orders_created = [] @@ -457,7 +469,7 @@ class BitgetTradingAPI: symbol=ccxt_symbol, type='stop_market', side=sl_side, - amount=contracts_amount, # 使用币数量,不是张数 + amount=btc_amount, price=None, params={ 'stopPrice': stop_loss, @@ -468,7 +480,7 @@ class BitgetTradingAPI: } ) orders_created.append(('止损', sl_order)) - logger.info(f"✅ 止损单已下: {sl_side} {contracts}张 ({contracts_amount}币) @ ${stop_loss}") + logger.info(f"✅ 止损单已下: {sl_side} {btc_amount} BTC @ ${stop_loss}") except Exception as e: logger.warning(f"下止损单失败: {e}") @@ -481,7 +493,7 @@ class BitgetTradingAPI: symbol=ccxt_symbol, type='limit', side=tp_side, - amount=contracts_amount, # 使用币数量,不是张数 + amount=btc_amount, price=take_profit, params={ 'tdMode': 'cross', @@ -490,7 +502,7 @@ class BitgetTradingAPI: } ) orders_created.append(('止盈', tp_order)) - logger.info(f"✅ 止盈单已下: {tp_side} {contracts}张 ({contracts_amount}币) @ ${take_profit}") + logger.info(f"✅ 止盈单已下: {tp_side} {btc_amount} BTC @ ${take_profit}") except Exception as e: logger.warning(f"下止盈单失败: {e}") @@ -779,6 +791,50 @@ class BitgetTradingAPI: # 默认返回原值 return symbol + def _get_contract_size(self, symbol: str) -> float: + """ + 获取合约面值(每张合约对应的币数量) + + Args: + symbol: 交易对 + + Returns: + 合约面值 + """ + # Bitget 永续合约规格 + if 'BTC' in symbol: + return 0.01 + elif 'ETH' in symbol: + return 0.1 + elif 'SOL' in symbol: + return 1.0 + elif 'BNB' in symbol: + return 0.1 + elif 'XRP' in symbol: + return 10.0 + elif 'DOGE' in symbol: + return 100.0 + elif 'MATIC' in symbol or 'POL' in symbol: + return 10.0 + elif 'AVAX' in symbol: + return 1.0 + elif 'LINK' in symbol: + return 1.0 + elif 'UNI' in symbol: + return 1.0 + elif 'ATOM' in symbol: + return 1.0 + elif 'LTC' in symbol: + return 0.1 + elif 'BCH' in symbol: + return 0.1 + elif 'FIL' in symbol: + return 1.0 + elif 'DOT' in symbol: + return 1.0 + else: + return 1.0 + def test_connection(self) -> bool: """ 测试 API 连接 diff --git a/backend/tests/test_bitget_live_integration.py b/backend/tests/test_bitget_live_integration.py new file mode 100644 index 0000000..742b0f5 --- /dev/null +++ b/backend/tests/test_bitget_live_integration.py @@ -0,0 +1,625 @@ +""" +Bitget 真实 API 集成测试 + +⚠️ 警告:此测试会使用真实 API 调用和真实订单! +- 确保使用测试网或接受小额手续费 +- 市价单会立即成交,产生实际盈亏 +- 测试后检查是否有残留订单/持仓 + +运行方式: + cd backend + source venv/bin/activate + python3 tests/test_bitget_live_integration.py +""" +import os +import sys +import time +from datetime import datetime + +# 添加项目路径 +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from dotenv import load_dotenv +load_dotenv('/Users/aaron/source_code/Stock_Agent/.env') + +from app.services.bitget_trading_api_sdk import BitgetTradingAPI +from app.services.bitget_live_trading_service import BitgetLiveTradingService + + +class BitgetIntegrationTest: + """Bitget 真实 API 集成测试""" + + def __init__(self): + self.api_key = os.getenv('BITGET_API_KEY') + self.api_secret = os.getenv('BITGET_API_SECRET') + self.passphrase = os.getenv('BITGET_PASSPHRASE') + self.use_testnet = os.getenv('BITGET_USE_TESTNET', 'true').lower() == 'true' + + if not all([self.api_key, self.api_secret, self.passphrase]): + raise ValueError("❌ 请在 .env 中配置 BITGET_API_KEY, BITGET_API_SECRET, BITGET_PASSPHRASE") + + print(f"\n{'='*60}") + print(f"Bitget 真实 API 集成测试") + print(f"{'='*60}") + print(f"API Key: {self.api_key[:10]}...") + print(f"测试网模式: {self.use_testnet}") + print(f"时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") + print(f"{'='*60}\n") + + # 初始化 API + self.api = BitgetTradingAPI( + api_key=self.api_key, + api_secret=self.api_secret, + passphrase=self.passphrase, + use_testnet=self.use_testnet + ) + + # 初始化 Service + self.service = BitgetLiveTradingService.__new__(BitgetLiveTradingService) + self.service.trading_api = self.api + self.service.max_single_position = 100 + self.service.max_total_leverage = 5 + self.service.circuit_breaker_drawdown = 0.10 + self.service._initial_account_state = None + self.service.initial_balance = None # 添加这个属性 + + # 测试状态 + self.limit_order_id = None + self.limit_order_symbol = None + self.market_entry_price = 0.0 + + # ==================== 基础连接测试 ==================== + + def test_01_connection(self): + """测试 1: API 连接""" + print("\n📡 测试 1: API 连接") + print("-" * 40) + + try: + # 获取服务器时间 + server_time = self.api.exchange.fetch_time() + print(f"✅ 服务器时间: {datetime.fromtimestamp(server_time/1000)}") + + # 加载市场信息 + markets = self.api.exchange.load_markets() + btc_market = markets.get('BTC/USDT:USDT', {}) + print(f"✅ BTC 市场ID: {btc_market.get('id', 'N/A')}") + + return True + except Exception as e: + print(f"❌ 连接失败: {e}") + return False + + def test_02_get_balance(self): + """测试 2: 获取账户余额""" + print("\n💰 测试 2: 获取账户余额") + print("-" * 40) + + try: + balance = self.api.get_balance() + if balance: + usdt = balance.get('USDT', {}) + print(f"✅ USDT 可用: {usdt.get('free', 0):.2f}") + print(f"✅ USDT 冻结: {usdt.get('used', 0):.2f}") + print(f"✅ USDT 总额: {usdt.get('total', 0):.2f}") + return True + else: + print("❌ 余额为空") + return False + except Exception as e: + print(f"❌ 获取余额失败: {e}") + return False + + def test_03_get_positions(self): + """测试 3: 获取当前持仓""" + print("\n📊 测试 3: 获取当前持仓") + print("-" * 40) + + try: + positions = self.api.get_position() + if positions: + active_positions = [p for p in positions if float(p.get('contracts', 0)) > 0] + print(f"✅ 当前持仓数: {len(active_positions)}") + for pos in active_positions: + symbol = pos.get('symbol', 'N/A') + contracts = float(pos.get('contracts', 0)) + side = pos.get('side', 'N/A') + entry = float(pos.get('entryPrice', 0)) + print(f" - {symbol}: {side} {contracts} 张 @ ${entry:,.2f}") + else: + print("✅ 当前无持仓") + return True + except Exception as e: + print(f"❌ 获取持仓失败: {e}") + return False + + def test_04_set_leverage(self): + """测试 4: 设置杠杆""" + print("\n⚙️ 测试 4: 设置杠杆") + print("-" * 40) + + try: + symbol = 'BTCUSDT' + leverage = 5 + self.api.set_leverage(symbol, leverage) + print(f"✅ {symbol} 杠杆已设置为 {leverage}x") + return True + except Exception as e: + print(f"❌ 设置杠杆失败: {e}") + return False + + # ==================== 限价单测试 ==================== + + def test_05_place_limit_order(self): + """测试 5: 下限价单(挂单,预期不成交)""" + print("\n📝 测试 5: 下限价单") + print("-" * 40) + + try: + symbol = 'BTCUSDT' + + # 获取当前价格 + ticker = self.api.exchange.fetch_ticker('BTC/USDT:USDT') + current_price = ticker['last'] + print(f" 当前 BTC 价格: ${current_price:,.2f}") + + # 计算挂单价格(当前价格 - 5%,预期不会成交) + limit_price = current_price * 0.95 + print(f" 挂单价格: ${limit_price:,.2f} (低 5%,预期不成交)") + + # 下 1 张多单(最小单位) + order = self.api.place_order( + symbol=symbol, + side='buy', + order_type='limit', + size=1, # 1 张 = 0.01 BTC + price=limit_price + ) + + if order: + self.limit_order_id = order.get('id') + self.limit_order_symbol = symbol + print(f"✅ 限价单已下: {self.limit_order_id}") + print(f" 订单状态: {order.get('status')}") + return True + else: + print("❌ 下单返回空") + return False + + except Exception as e: + print(f"❌ 下限价单失败: {e}") + return False + + def test_06_get_open_orders(self): + """测试 6: 查询挂单""" + print("\n📋 测试 6: 查询挂单") + print("-" * 40) + + try: + symbol = self.limit_order_symbol or 'BTCUSDT' + orders = self.api.get_open_orders(symbol) + + if orders: + print(f"✅ 当前挂单数: {len(orders)}") + for order in orders: + order_id = order.get('id', 'N/A') + side = order.get('side', 'N/A') + price = order.get('price', 0) + amount = order.get('amount', 0) + print(f" - {order_id}: {side} {amount} @ ${price:,.2f}") + else: + print("⚠️ 无挂单") + return True + + except Exception as e: + print(f"❌ 查询挂单失败: {e}") + return False + + def test_07_cancel_order(self): + """测试 7: 撤销挂单""" + print("\n❌ 测试 7: 撤销挂单") + print("-" * 40) + + try: + if not self.limit_order_id: + print("⚠️ 没有需要撤销的订单") + return True + + # 使用 cancel_all_orders 撤销该 symbol 所有挂单 + success = self.api.cancel_all_orders(self.limit_order_symbol) + + if success: + print(f"✅ 已撤销 {self.limit_order_symbol} 的所有挂单") + self.limit_order_id = None + return True + else: + print(f"❌ 撤单失败") + return False + + except Exception as e: + print(f"❌ 撤销订单失败: {e}") + return False + + # ==================== 市价单测试(真实成交!)==================== + + def test_08_market_order_open(self): + """测试 8: 市价开多单(真实成交!)""" + print("\n⚡ 测试 8: 市价开多单") + print("-" * 40) + print("⚠️ 警告:此测试会产生真实订单和手续费!") + + try: + symbol = 'BTCUSDT' + + # 获取当前价格 + ticker = self.api.exchange.fetch_ticker('BTC/USDT:USDT') + current_price = ticker['last'] + print(f" 当前 BTC 价格: ${current_price:,.2f}") + + # 下 1 张市价多单(最小单位 = 0.01 BTC) + print(f" 下市价多单: 1 张 (0.01 BTC)...") + order = self.api.place_order( + symbol=symbol, + side='buy', + order_type='market', + size=1 # 1 张 = 0.01 BTC + ) + + if order: + order_id = order.get('id') + status = order.get('status') + # 安全获取 average 价格 + avg_price = order.get('average') or order.get('price') or 0 + if avg_price: + avg_price = float(avg_price) + print(f"✅ 市价单已成交: {order_id}") + print(f" 订单状态: {status}") + print(f" 成交均价: ${avg_price:,.2f}") + print(f" 成交数量: {order.get('filled', 0)} 张") + + # 保存入场价 + self.market_entry_price = avg_price + return True + else: + print("❌ 下单返回空") + return False + + except Exception as e: + print(f"❌ 市价开仓失败: {e}") + import traceback + traceback.print_exc() + return False + + def test_09_verify_position_opened(self): + """测试 9: 验证持仓已开启""" + print("\n🔍 测试 9: 验证持仓") + print("-" * 40) + + try: + positions = self.api.get_position('BTCUSDT') + + if positions: + for pos in positions: + contracts = float(pos.get('contracts', 0)) + if contracts > 0: + side = pos.get('side', 'N/A') + entry_price = float(pos.get('entryPrice', 0)) + unrealized_pnl = float(pos.get('unrealizedPnl', 0)) + print(f"✅ 持仓已确认:") + print(f" 方向: {side}") + print(f" 数量: {contracts} 张") + print(f" 开仓价: ${entry_price:,.2f}") + print(f" 未实现盈亏: ${unrealized_pnl:,.2f}") + return True + + print("❌ 未找到持仓") + return False + + except Exception as e: + print(f"❌ 验证持仓失败: {e}") + return False + + def test_10_set_tp_sl(self): + """测试 10: 设置止盈止损""" + print("\n🎯 测试 10: 设置止盈止损") + print("-" * 40) + + try: + # 获取当前价格 + ticker = self.api.exchange.fetch_ticker('BTC/USDT:USDT') + current_price = ticker['last'] + + # 设置止盈(+1%)和止损(-0.5%) + tp_price = current_price * 1.01 # +1% + sl_price = current_price * 0.995 # -0.5% + + print(f" 当前价格: ${current_price:,.2f}") + print(f" 设置止盈: ${tp_price:,.2f} (+1%)") + print(f" 设置止损: ${sl_price:,.2f} (-0.5%)") + + result = self.api.modify_sl_tp( + symbol='BTCUSDT', + stop_loss=sl_price, + take_profit=tp_price + ) + + if result: + print(f"✅ 止盈止损已设置") + else: + print(f"⚠️ 止盈止损设置返回 False") + + return True # 无论成功与否都继续测试 + + except Exception as e: + print(f"⚠️ 设置止盈止损失败: {e}") + return True # 不算失败,继续测试 + + def test_11_market_order_close(self): + """测试 11: 市价平仓""" + print("\n🔒 测试 11: 市价平仓") + print("-" * 40) + + try: + # 获取当前价格 + ticker = self.api.exchange.fetch_ticker('BTC/USDT:USDT') + current_price = ticker['last'] + print(f" 当前 BTC 价格: ${current_price:,.2f}") + + # 先撤销止盈止损单 + try: + self.api.cancel_all_orders('BTCUSDT') + print(" 已撤销止盈止损单") + except: + pass + + # 使用 close_position 方法平仓 + print(f" 下市价平仓单...") + order = self.api.close_position( + symbol='BTCUSDT', + side='sell', # 平多 + size=0.01, # 0.01 张 + ) + + if order: + order_id = order.get('id') + avg_price = order.get('average') or order.get('price') or 0 + if avg_price: + avg_price = float(avg_price) + print(f"✅ 平仓成功: {order_id}") + print(f" 平仓均价: ${avg_price:,.2f}") + + # 计算盈亏 + if self.market_entry_price > 0 and avg_price > 0: + pnl = (avg_price - self.market_entry_price) * 0.01 # 1张 = 0.01 BTC + pnl_pct = (avg_price - self.market_entry_price) / self.market_entry_price * 100 + print(f" 实现盈亏: ${pnl:,.2f} ({pnl_pct:+.2f}%)") + + return True + else: + print("❌ 平仓返回空") + return False + + except Exception as e: + print(f"❌ 市价平仓失败: {e}") + import traceback + traceback.print_exc() + return False + + def test_12_verify_position_closed(self): + """测试 12: 验证持仓已平""" + print("\n✅ 测试 12: 验证持仓已平") + print("-" * 40) + + try: + positions = self.api.get_position('BTCUSDT') + + has_position = False + if positions: + for pos in positions: + if float(pos.get('contracts', 0)) > 0: + has_position = True + print(f"⚠️ 仍有持仓: {pos.get('contracts')} 张") + + if not has_position: + print("✅ 持仓已全部平仓") + return True + else: + print("❌ 持仓未完全平仓") + return False + + except Exception as e: + print(f"❌ 验证平仓失败: {e}") + return False + + # ==================== Service 层测试 ==================== + + def test_13_service_get_account_state(self): + """测试 13: Service 层获取账户状态""" + print("\n🏦 测试 13: Service 获取账户状态") + print("-" * 40) + + try: + state = self.service.get_account_state() + print(f"✅ 账户价值: ${state['account_value']:,.2f}") + print(f"✅ 已用保证金: ${state['total_margin_used']:,.2f}") + print(f"✅ 可用余额: ${state['available_balance']:,.2f}") + return True + except Exception as e: + print(f"❌ 获取账户状态失败: {e}") + import traceback + traceback.print_exc() + return False + + def test_14_service_get_positions(self): + """测试 14: Service 层获取持仓""" + print("\n📈 测试 14: Service 获取持仓") + print("-" * 40) + + try: + positions = self.service.get_open_positions() + if positions: + print(f"✅ 当前持仓数: {len(positions)}") + for pos in positions: + # 安全获取字段 + symbol = pos.get('symbol', 'N/A') + side = pos.get('side', 'N/A') + size = pos.get('size', 0) + entry = pos.get('entry_price', 0) + print(f" - {symbol}: {side} {size} @ ${entry:,.2f}") + else: + print("✅ 当前无持仓") + return True + except Exception as e: + print(f"❌ 获取持仓失败: {e}") + import traceback + traceback.print_exc() + return False + + def test_15_service_contract_size(self): + """测试 15: Service 合约面值""" + print("\n📐 测试 15: 合约面值换算") + print("-" * 40) + + try: + # 测试已知币种 + btc_size = self.service.get_contract_size('BTC') + eth_size = self.service.get_contract_size('ETH') + sol_size = self.service.get_contract_size('SOL') + + print(f"✅ BTC 合约面值: {btc_size} BTC/张") + print(f"✅ ETH 合约面值: {eth_size} ETH/张") + print(f"✅ SOL 合约面值: {sol_size} SOL/张") + + # 测试币数转合约数 + contracts = self.service.coins_to_contracts('BTC', 0.05) # 0.05 BTC + print(f"✅ 0.05 BTC = {contracts} 张") + + return True + except Exception as e: + print(f"❌ 合约面值测试失败: {e}") + return False + + def test_16_risk_check(self): + """测试 16: 风控检查""" + print("\n⚠️ 测试 16: 风控检查") + print("-" * 40) + + try: + result = self.service.check_risk_limits() + if result['allowed']: + print(f"✅ 风控检查通过") + else: + print(f"⚠️ 风控检查未通过: {result['reason']}") + return True + except Exception as e: + print(f"⚠️ 风控检查异常: {e}") + return True # 不算失败 + + # ==================== 清理和运行 ==================== + + def cleanup(self): + """清理测试产生的订单和持仓""" + print("\n🧹 清理测试订单...") + print("-" * 40) + + try: + # 撤销所有 BTC 挂单 + success = self.api.cancel_all_orders('BTCUSDT') + if success: + print("✅ 已撤销所有 BTC 挂单") + else: + print("⚠️ 撤销 BTC 挂单返回 False(可能无挂单)") + + # 检查是否有残留持仓 + positions = self.api.get_position('BTCUSDT') + if positions: + for pos in positions: + contracts = float(pos.get('contracts', 0)) + if contracts > 0: + symbol = pos.get('symbol', '') + print(f"⚠️ 发现残留持仓: {symbol} {contracts} 张") + print(f" 请手动平仓!") + + except Exception as e: + print(f"⚠️ 清理失败: {e}") + + def run_all_tests(self): + """运行所有测试""" + tests = [ + self.test_01_connection, + self.test_02_get_balance, + self.test_03_get_positions, + self.test_04_set_leverage, + self.test_05_place_limit_order, + self.test_06_get_open_orders, + self.test_07_cancel_order, + self.test_08_market_order_open, + self.test_09_verify_position_opened, + self.test_10_set_tp_sl, + self.test_11_market_order_close, + self.test_12_verify_position_closed, + self.test_13_service_get_account_state, + self.test_14_service_get_positions, + self.test_15_service_contract_size, + self.test_16_risk_check, + ] + + results = [] + for test in tests: + try: + result = test() + results.append((test.__name__, result)) + time.sleep(0.5) # 避免触发频率限制 + except Exception as e: + print(f"\n❌ 测试异常: {e}") + import traceback + traceback.print_exc() + results.append((test.__name__, False)) + + # 清理 + self.cleanup() + + # 汇总 + print("\n" + "=" * 60) + print("测试结果汇总") + print("=" * 60) + + passed = sum(1 for _, r in results if r) + total = len(results) + + for name, result in results: + status = "✅ PASS" if result else "❌ FAIL" + print(f"{status}: {name}") + + print(f"\n总计: {passed}/{total} 通过") + print("=" * 60) + + return passed == total + + +if __name__ == '__main__': + print("\n" + "!" * 60) + print("⚠️ 警告:此测试将使用真实 API 调用!") + print("!" * 60) + print("请确认:") + print("1. 你已了解这会产生真实 API 调用") + print("2. 你使用的是测试网或接受小额手续费") + print("3. 市价单会立即成交,产生实际盈亏") + print("4. 测试后请检查是否有残留订单/持仓") + print("!" * 60) + + confirm = input("\n是否继续?(yes/no): ") + + if confirm.lower() != 'yes': + print("已取消测试") + sys.exit(0) + + try: + tester = BitgetIntegrationTest() + success = tester.run_all_tests() + sys.exit(0 if success else 1) + except Exception as e: + print(f"\n❌ 测试失败: {e}") + import traceback + traceback.print_exc() + sys.exit(1)