""" 账户实时数据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 stop_loss_price = None take_profit_price = None take_profit_1 = None take_profit_2 = None atr_value = None db_margin_usdt = None db_notional_usdt = 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') # 尝试从数据库获取止损止盈价格(如果存储了) stop_loss_price = db_trade.get('stop_loss_price') take_profit_price = db_trade.get('take_profit_price') take_profit_1 = db_trade.get('take_profit_1') take_profit_2 = db_trade.get('take_profit_2') atr_value = db_trade.get('atr') db_margin_usdt = db_trade.get('margin_usdt') db_notional_usdt = db_trade.get('notional_usdt') 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": entry_value_usdt, # 新字段:名义/保证金(若DB有则优先使用DB;否则使用实时计算) "notional_usdt": db_notional_usdt if db_notional_usdt is not None else entry_value_usdt, "margin_usdt": db_margin_usdt if db_margin_usdt is not None else margin, "mark_price": mark_price, "pnl": unrealized_pnl, "pnl_percent": pnl_percent, # 基于保证金的盈亏百分比 "leverage": int(pos.get('leverage', 1)), "entry_time": entry_time, # 开仓时间 "stop_loss_price": stop_loss_price, # 止损价格(如果可用) "take_profit_price": take_profit_price, # 止盈价格(如果可用) "take_profit_1": take_profit_1, "take_profit_2": take_profit_2, "atr": atr_value, }) 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 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 logger.info("✓ 从trading_system路径导入成功") # 导入数据库模型 from database.models import Trade # 创建客户端 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(f"检查 {symbol} 在币安的持仓状态...") positions = await client.get_open_positions() position = next((p for p in positions if p['symbol'] == symbol and float(p['positionAmt']) != 0), None) if not position: logger.warning(f"⚠ {symbol} 币安账户中没有持仓,可能已被平仓") # 检查数据库中是否有未平仓的记录,如果有则更新 open_trades = Trade.get_by_symbol(symbol, status='open') if open_trades: trade = open_trades[0] # 获取当前价格作为平仓价格 ticker = await client.get_ticker_24h(symbol) exit_price = float(ticker['price']) if ticker else float(trade['entry_price']) # 计算盈亏 entry_price = float(trade['entry_price']) quantity = float(trade['quantity']) if trade['side'] == 'BUY': pnl = (exit_price - entry_price) * quantity pnl_percent = ((exit_price - entry_price) / entry_price) * 100 else: pnl = (entry_price - exit_price) * quantity pnl_percent = ((entry_price - exit_price) / entry_price) * 100 # 更新数据库 Trade.update_exit( trade_id=trade['id'], exit_price=exit_price, exit_reason='manual', pnl=pnl, pnl_percent=pnl_percent, exit_order_id=None ) logger.info(f"✓ 已更新数据库记录(币安无持仓但数据库有记录)") return { "message": f"{symbol} 平仓操作完成(币安账户中没有持仓,可能已被平仓)", "symbol": symbol, "status": "closed" } # 获取持仓信息 position_amt = float(position['positionAmt']) logger.info(f"✓ 币安账户中有 {symbol} 持仓: {position_amt:.4f}") # 确定平仓方向(与持仓相反) side = 'SELL' if position_amt > 0 else 'BUY' quantity = abs(position_amt) logger.info(f"开始执行平仓操作: {symbol} {side} {quantity:.4f} @ MARKET (reduceOnly=true)...") # 直接调用币安 API 平仓,绕过 BinanceClient 的检查逻辑 # 对于平仓操作,应该直接使用币安 API,不需要名义价值和保证金检查 try: # 获取交易对精度信息,调整数量精度 symbol_info = await client.get_symbol_info(symbol) if symbol_info: quantity_precision = symbol_info.get('quantityPrecision', 8) step_size = symbol_info.get('stepSize', 0) min_qty = symbol_info.get('minQty', 0) # 调整数量精度 if step_size > 0: adjusted_quantity = float(int(quantity / step_size)) * step_size else: adjusted_quantity = round(quantity, quantity_precision) # 确保不小于最小数量 if min_qty > 0 and adjusted_quantity < min_qty: adjusted_quantity = min_qty adjusted_quantity = round(adjusted_quantity, quantity_precision) if adjusted_quantity != quantity: logger.info(f"数量精度调整: {quantity} -> {adjusted_quantity}") quantity = adjusted_quantity # 直接调用币安 API 下单(使用 reduceOnly="true") order = await client.client.futures_create_order( symbol=symbol, side=side, type='MARKET', quantity=quantity, reduceOnly="true" # 使用字符串格式,符合币安API要求 ) if not order: error_msg = f"{symbol} 平仓失败:币安API返回 None" logger.error(error_msg) raise HTTPException(status_code=500, detail=error_msg) except Exception as order_error: error_msg = f"{symbol} 平仓失败:下单异常 - {str(order_error)}" logger.error(error_msg) logger.error(f" 错误类型: {type(order_error).__name__}") import traceback logger.error(f" 完整错误堆栈:\n{traceback.format_exc()}") raise HTTPException(status_code=500, detail=error_msg) order_id = order.get('orderId') logger.info(f"✓ {symbol} 平仓订单已提交 (订单ID: {order_id})") # 等待订单成交,获取实际成交价格 import asyncio await asyncio.sleep(1) # 获取订单详情 exit_price = None try: order_info = await client.client.futures_get_order(symbol=symbol, orderId=order_id) if order_info: exit_price = float(order_info.get('avgPrice', 0)) or float(order_info.get('price', 0)) if exit_price <= 0 and order_info.get('fills'): # 计算加权平均成交价格 total_qty = 0 total_value = 0 for fill in order_info.get('fills', []): qty = float(fill.get('qty', 0)) price = float(fill.get('price', 0)) total_qty += qty total_value += qty * price if total_qty > 0: exit_price = total_value / total_qty except Exception as e: logger.warning(f"获取订单详情失败: {e},使用当前价格") # 如果无法获取订单价格,使用当前价格 if not exit_price or exit_price <= 0: ticker = await client.get_ticker_24h(symbol) exit_price = float(ticker['price']) if ticker else float(position.get('entryPrice', 0)) # 更新数据库记录 open_trades = Trade.get_by_symbol(symbol, status='open') if open_trades: trade = open_trades[0] entry_price = float(trade['entry_price']) trade_quantity = float(trade['quantity']) # 计算盈亏 if trade['side'] == 'BUY': pnl = (exit_price - entry_price) * trade_quantity pnl_percent = ((exit_price - entry_price) / entry_price) * 100 else: pnl = (entry_price - exit_price) * trade_quantity pnl_percent = ((entry_price - exit_price) / entry_price) * 100 # 更新数据库 Trade.update_exit( trade_id=trade['id'], exit_price=exit_price, exit_reason='manual', pnl=pnl, pnl_percent=pnl_percent, exit_order_id=order_id ) logger.info(f"✓ 已更新数据库记录 (盈亏: {pnl:.2f} USDT, {pnl_percent:.2f}%)") logger.info(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) @router.post("/positions/sync") async def sync_positions(): """同步币安实际持仓状态与数据库状态""" try: logger.info("=" * 60) logger.info("收到持仓状态同步请求") logger.info("=" * 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 except ImportError: trading_system_path = project_root / 'trading_system' sys.path.insert(0, str(trading_system_path)) from binance_client import BinanceClient # 导入数据库模型 from database.models import Trade # 创建客户端 client = BinanceClient( api_key=api_key, api_secret=api_secret, testnet=use_testnet ) logger.info("连接币安API...") await client.connect() try: # 1. 获取币安实际持仓 binance_positions = await client.get_open_positions() binance_symbols = {p['symbol'] for p in binance_positions if float(p.get('positionAmt', 0)) != 0} logger.info(f"币安实际持仓: {len(binance_symbols)} 个") if binance_symbols: logger.info(f" 持仓列表: {', '.join(binance_symbols)}") # 2. 获取数据库中状态为open的交易记录 db_open_trades = Trade.get_all(status='open') db_open_symbols = {t['symbol'] for t in db_open_trades} logger.info(f"数据库open状态: {len(db_open_symbols)} 个") if db_open_symbols: logger.info(f" 持仓列表: {', '.join(db_open_symbols)}") # 3. 找出在数据库中open但在币安已不存在的持仓(需要更新为closed) missing_in_binance = db_open_symbols - binance_symbols updated_count = 0 if missing_in_binance: logger.info(f"发现 {len(missing_in_binance)} 个持仓在数据库中是open但币安已不存在: {', '.join(missing_in_binance)}") for symbol in missing_in_binance: try: # 获取该交易对的所有open记录 open_trades = Trade.get_by_symbol(symbol, status='open') for trade in open_trades: trade_id = trade['id'] entry_price = float(trade['entry_price']) quantity = float(trade['quantity']) # 获取当前价格作为平仓价格 ticker = await client.get_ticker_24h(symbol) exit_price = float(ticker['price']) if ticker else entry_price # 计算盈亏 if trade['side'] == 'BUY': pnl = (exit_price - entry_price) * quantity else: pnl = (entry_price - exit_price) * quantity # 计算基于保证金的盈亏百分比 leverage = float(trade.get('leverage', 10)) entry_value = entry_price * quantity margin = entry_value / leverage if leverage > 0 else entry_value pnl_percent_margin = (pnl / margin * 100) if margin > 0 else 0 # 更新数据库记录 Trade.update_exit( trade_id=trade_id, exit_price=exit_price, exit_reason='sync', # 标记为同步平仓 pnl=pnl, pnl_percent=pnl_percent_margin, # 使用基于保证金的盈亏百分比 exit_order_id=None ) updated_count += 1 logger.info( f"✓ {symbol} 已更新为closed (ID: {trade_id}, " f"盈亏: {pnl:.2f} USDT, {pnl_percent_margin:.2f}% of margin)" ) except Exception as e: logger.error(f"❌ {symbol} 更新失败: {e}") import traceback logger.error(f" 错误详情:\n{traceback.format_exc()}") else: logger.info("✓ 数据库与币安状态一致,无需更新") # 4. 检查币安有但数据库没有记录的持仓 missing_in_db = binance_symbols - db_open_symbols if missing_in_db: logger.info(f"发现 {len(missing_in_db)} 个持仓在币安存在但数据库中没有记录: {', '.join(missing_in_db)}") logger.info(" 这些持仓可能是手动开仓的,建议手动处理") result = { "message": "持仓状态同步完成", "binance_positions": len(binance_symbols), "db_open_positions": len(db_open_symbols), "updated_to_closed": updated_count, "missing_in_binance": list(missing_in_binance), "missing_in_db": list(missing_in_db) } logger.info("=" * 60) logger.info("持仓状态同步完成!") logger.info(f"结果: {result}") logger.info("=" * 60) return result finally: await client.disconnect() logger.info("✓ 已断开币安API连接") 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)