""" 账户实时数据API - 从币安API获取实时账户和订单数据 """ from fastapi import APIRouter, HTTPException import sys from pathlib import Path import logging project_root = Path(__file__).parent.parent.parent.parent sys.path.insert(0, str(project_root)) sys.path.insert(0, str(project_root / 'backend')) sys.path.insert(0, str(project_root / 'trading_system')) from database.models import TradingConfig logger = logging.getLogger(__name__) router = APIRouter() async def get_realtime_account_data(): """从币安API实时获取账户数据""" logger.info("=" * 60) logger.info("开始获取实时账户数据") logger.info("=" * 60) try: # 从数据库读取API密钥 logger.info("步骤1: 从数据库读取API配置...") api_key = TradingConfig.get_value('BINANCE_API_KEY') api_secret = TradingConfig.get_value('BINANCE_API_SECRET') use_testnet = TradingConfig.get_value('USE_TESTNET', False) logger.info(f" - API密钥存在: {bool(api_key)}") if api_key: logger.info(f" - API密钥长度: {len(api_key)} 字符") logger.info(f" - API密钥前缀: {api_key[:10]}...") else: logger.warning(" - API密钥为空!") logger.info(f" - API密钥存在: {bool(api_secret)}") if api_secret: logger.info(f" - API密钥长度: {len(api_secret)} 字符") logger.info(f" - API密钥前缀: {api_secret[:10]}...") else: logger.warning(" - API密钥为空!") logger.info(f" - 使用测试网: {use_testnet}") if not api_key or not api_secret: error_msg = "API密钥未配置,请在配置界面设置BINANCE_API_KEY和BINANCE_API_SECRET" logger.error(f" ✗ {error_msg}") raise HTTPException( status_code=400, detail=error_msg ) # 导入交易系统的BinanceClient logger.info("步骤2: 导入BinanceClient...") try: from binance_client import BinanceClient logger.info(" ✓ 从当前路径导入BinanceClient成功") except ImportError as e: logger.warning(f" - 从当前路径导入失败: {e}") # 如果直接导入失败,尝试从trading_system导入 trading_system_path = project_root / 'trading_system' sys.path.insert(0, str(trading_system_path)) logger.info(f" - 添加路径到sys.path: {trading_system_path}") try: from binance_client import BinanceClient logger.info(" ✓ 从trading_system路径导入BinanceClient成功") except ImportError as e2: logger.error(f" ✗ 导入BinanceClient失败: {e2}") raise # 创建客户端 logger.info("步骤3: 创建BinanceClient实例...") client = BinanceClient( api_key=api_key, api_secret=api_secret, testnet=use_testnet ) logger.info(f" ✓ 客户端创建成功 (testnet={use_testnet})") # 连接币安API logger.info("步骤4: 连接币安API...") try: await client.connect() logger.info(" ✓ 币安API连接成功") except Exception as e: logger.error(f" ✗ 币安API连接失败: {e}", exc_info=True) raise # 获取账户余额 logger.info("步骤5: 获取账户余额...") try: balance = await client.get_account_balance() logger.info(" ✓ 账户余额获取成功") logger.info(f" - 返回数据类型: {type(balance)}") logger.info(f" - 返回数据内容: {balance}") if balance: logger.info(f" - 总余额: {balance.get('total', 'N/A')} USDT") logger.info(f" - 可用余额: {balance.get('available', 'N/A')} USDT") logger.info(f" - 保证金: {balance.get('margin', 'N/A')} USDT") if balance.get('total', 0) == 0: logger.warning(" ⚠ 账户余额为0,可能是API权限问题或账户确实无余额") else: logger.warning(" ⚠ 返回的余额数据为空") except Exception as e: logger.error(f" ✗ 获取账户余额失败: {e}", exc_info=True) raise # 获取持仓 logger.info("步骤6: 获取持仓信息...") try: positions = await client.get_open_positions() logger.info(" ✓ 持仓信息获取成功") logger.info(f" - 返回数据类型: {type(positions)}") logger.info(f" - 持仓数量: {len(positions)}") if positions: logger.info(" - 持仓详情:") for i, pos in enumerate(positions[:5], 1): # 只显示前5个 logger.info(f" {i}. {pos.get('symbol', 'N/A')}: " f"数量={pos.get('positionAmt', 0)}, " f"入场价={pos.get('entryPrice', 0)}, " f"盈亏={pos.get('unRealizedProfit', 0)}") if len(positions) > 5: logger.info(f" ... 还有 {len(positions) - 5} 个持仓") else: logger.info(" - 当前无持仓") except Exception as e: logger.error(f" ✗ 获取持仓信息失败: {e}", exc_info=True) raise # 计算总仓位价值和总盈亏 logger.info("步骤7: 计算仓位统计...") total_position_value = 0 total_pnl = 0 open_positions_count = 0 for pos in positions: position_amt = float(pos.get('positionAmt', 0)) if position_amt == 0: continue entry_price = float(pos.get('entryPrice', 0)) mark_price = float(pos.get('markPrice', 0)) unrealized_pnl = float(pos.get('unRealizedProfit', 0)) if mark_price == 0: # 如果没有标记价格,使用入场价 mark_price = entry_price position_value = abs(position_amt * mark_price) total_position_value += position_value total_pnl += unrealized_pnl open_positions_count += 1 logger.debug(f" - {pos.get('symbol')}: 价值={position_value:.2f}, 盈亏={unrealized_pnl:.2f}") logger.info(" ✓ 仓位统计计算完成") logger.info(f" - 总仓位价值: {total_position_value:.2f} USDT") logger.info(f" - 总盈亏: {total_pnl:.2f} USDT") logger.info(f" - 持仓数量: {open_positions_count}") # 断开连接 logger.info("步骤8: 断开币安API连接...") try: await client.disconnect() logger.info(" ✓ 连接已断开") except Exception as e: logger.warning(f" ⚠ 断开连接时出错: {e}") # 构建返回结果 result = { "total_balance": balance.get('total', 0) if balance else 0, "available_balance": balance.get('available', 0) if balance else 0, "total_position_value": total_position_value, "total_pnl": total_pnl, "open_positions": open_positions_count } logger.info("=" * 60) logger.info("账户数据获取成功!") logger.info(f"最终结果: {result}") logger.info("=" * 60) return result except HTTPException as e: logger.error("=" * 60) logger.error(f"HTTP异常: {e.status_code} - {e.detail}") logger.error("=" * 60) raise except Exception as e: error_msg = f"获取账户数据失败: {str(e)}" logger.error("=" * 60) logger.error(f"异常类型: {type(e).__name__}") logger.error(f"错误信息: {error_msg}") logger.error("=" * 60, exc_info=True) raise HTTPException(status_code=500, detail=error_msg) @router.get("/realtime") async def get_realtime_account(): """获取实时账户数据""" return await get_realtime_account_data() @router.get("/positions") async def get_realtime_positions(): """获取实时持仓数据""" try: # 从数据库读取API密钥 api_key = TradingConfig.get_value('BINANCE_API_KEY') api_secret = TradingConfig.get_value('BINANCE_API_SECRET') use_testnet = TradingConfig.get_value('USE_TESTNET', False) logger.info(f"尝试获取实时持仓数据 (testnet={use_testnet})") if not api_key or not api_secret: error_msg = "API密钥未配置" logger.warning(error_msg) raise HTTPException( status_code=400, detail=error_msg ) # 导入BinanceClient try: from binance_client import BinanceClient except ImportError: trading_system_path = project_root / 'trading_system' sys.path.insert(0, str(trading_system_path)) from binance_client import BinanceClient client = BinanceClient( api_key=api_key, api_secret=api_secret, testnet=use_testnet ) logger.info("连接币安API获取持仓...") await client.connect() positions = await client.get_open_positions() await client.disconnect() logger.info(f"获取到 {len(positions)} 个持仓") # 格式化持仓数据 formatted_positions = [] for pos in positions: position_amt = float(pos.get('positionAmt', 0)) if position_amt == 0: continue entry_price = float(pos.get('entryPrice', 0)) mark_price = float(pos.get('markPrice', 0)) unrealized_pnl = float(pos.get('unRealizedProfit', 0)) if mark_price == 0: mark_price = entry_price position_value = abs(position_amt * mark_price) # 计算开仓时的USDT数量(名义价值) entry_value_usdt = abs(position_amt) * entry_price # 计算收益率:盈亏 / 保证金(与币安一致) # 保证金 = 名义价值 / 杠杆 leverage = float(pos.get('leverage', 1)) margin = entry_value_usdt / leverage if leverage > 0 else entry_value_usdt pnl_percent = 0 if margin > 0: pnl_percent = (unrealized_pnl / margin) * 100 # 尝试从数据库获取开仓时间 entry_time = None try: from database.models import Trade db_trades = Trade.get_by_symbol(pos.get('symbol'), status='open') if db_trades: # 找到匹配的交易记录(通过symbol和entry_price匹配) for db_trade in db_trades: if abs(float(db_trade.get('entry_price', 0)) - entry_price) < 0.01: entry_time = db_trade.get('entry_time') break except Exception as e: logger.debug(f"获取开仓时间失败: {e}") formatted_positions.append({ "symbol": pos.get('symbol'), "side": "BUY" if position_amt > 0 else "SELL", "quantity": abs(position_amt), "entry_price": entry_price, "entry_value_usdt": entry_value_usdt, # 开仓时的USDT数量 "mark_price": mark_price, "pnl": unrealized_pnl, "pnl_percent": pnl_percent, "leverage": int(pos.get('leverage', 1)), "entry_time": entry_time # 开仓时间 }) logger.info(f"格式化后 {len(formatted_positions)} 个有效持仓") return formatted_positions except HTTPException: raise except Exception as e: error_msg = f"获取持仓数据失败: {str(e)}" logger.error(error_msg, exc_info=True) raise HTTPException(status_code=500, detail=error_msg) @router.post("/positions/{symbol}/close") async def close_position(symbol: str): """手动平仓指定交易对的持仓""" try: logger.info(f"=" * 60) logger.info(f"收到平仓请求: {symbol}") logger.info(f"=" * 60) # 从数据库读取API密钥 api_key = TradingConfig.get_value('BINANCE_API_KEY') api_secret = TradingConfig.get_value('BINANCE_API_SECRET') use_testnet = TradingConfig.get_value('USE_TESTNET', False) if not api_key or not api_secret: error_msg = "API密钥未配置" logger.warning(error_msg) raise HTTPException(status_code=400, detail=error_msg) # 导入必要的模块 try: from binance_client import BinanceClient from risk_manager import RiskManager from position_manager import PositionManager logger.info("✓ 成功导入交易系统模块") except ImportError as import_error: logger.warning(f"首次导入失败: {import_error},尝试从trading_system路径导入") trading_system_path = project_root / 'trading_system' sys.path.insert(0, str(trading_system_path)) from binance_client import BinanceClient from risk_manager import RiskManager from position_manager import PositionManager logger.info("✓ 从trading_system路径导入成功") # 创建客户端和仓位管理器 logger.info(f"创建BinanceClient (testnet={use_testnet})...") client = BinanceClient( api_key=api_key, api_secret=api_secret, testnet=use_testnet ) logger.info("连接币安API...") await client.connect() logger.info("✓ 币安API连接成功") try: logger.info("初始化RiskManager和PositionManager...") risk_manager = RiskManager(client) position_manager = PositionManager(client, risk_manager) logger.info("✓ 管理器初始化成功") # 先检查币安是否有持仓 logger.info(f"检查 {symbol} 在币安的持仓状态...") positions = await client.get_open_positions() binance_has_position = any(p['symbol'] == symbol and float(p['positionAmt']) != 0 for p in positions) if binance_has_position: position_info = next((p for p in positions if p['symbol'] == symbol and float(p['positionAmt']) != 0), None) position_amt = float(position_info['positionAmt']) if position_info else 0 logger.info(f"✓ 币安账户中有 {symbol} 持仓: {position_amt:.4f}") else: logger.info(f"✓ 币安账户中没有 {symbol} 持仓") # 执行平仓(reason='manual' 表示手动平仓) logger.info(f"开始执行平仓操作: {symbol}...") success = await position_manager.close_position(symbol, reason='manual') if success: logger.info(f"✓ {symbol} 平仓成功") return { "message": f"{symbol} 平仓成功", "symbol": symbol, "status": "closed" } else: # 根据币安是否有持仓来判断失败原因 if binance_has_position: # 币安有持仓但下单失败 error_msg = f"{symbol} 平仓失败:币安账户中有持仓,但下单失败,请检查日志或稍后重试" logger.error(error_msg) raise HTTPException(status_code=500, detail=error_msg) else: # 币安没有持仓,可能是数据库已更新 logger.warning(f"⚠ {symbol} 币安账户中没有持仓,可能已被平仓或数据库已更新") return { "message": f"{symbol} 平仓操作完成(币安账户中没有持仓,可能已被平仓)", "symbol": symbol, "status": "closed" } finally: logger.info("断开币安API连接...") await client.disconnect() logger.info("✓ 已断开连接") except HTTPException: raise except Exception as e: error_msg = f"平仓失败: {str(e)}" logger.error("=" * 60) logger.error(f"平仓操作异常: {error_msg}") logger.error(f"错误类型: {type(e).__name__}") logger.error("=" * 60, exc_info=True) raise HTTPException(status_code=500, detail=error_msg)