auto_trade_sys/backend/api/routes/account.py
薇薇安 4d0ef0f76c a
2026-01-16 16:37:43 +08:00

504 lines
21 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.

"""
账户实时数据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
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...")
# 直接调用 BinanceClient 平仓(使用 reduceOnly=True
order = await client.place_order(
symbol=symbol,
side=side,
quantity=quantity,
order_type='MARKET',
reduce_only=True
)
if not order:
error_msg = f"{symbol} 平仓失败:下单返回 None请检查日志"
logger.error(error_msg)
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)