689 lines
30 KiB
Python
689 lines
30 KiB
Python
"""
|
||
账户实时数据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
|
||
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')
|
||
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, # 开仓时间
|
||
"stop_loss_price": stop_loss_price, # 止损价格(如果可用)
|
||
"take_profit_price": take_profit_price # 止盈价格(如果可用)
|
||
})
|
||
|
||
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)
|