"""
账户实时数据API - 从币安API获取实时账户和订单数据
"""
from fastapi import APIRouter, HTTPException, Header, Depends
from fastapi import Query
import asyncio
import sys
from pathlib import Path
import logging
import time

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, Account
from api.auth_deps import get_account_id

logger = logging.getLogger(__name__)
router = APIRouter()


async def _ensure_exchange_sltp_for_symbol(symbol: str, account_id: int = 1):
    """
    在币安侧补挂该 symbol 的止损/止盈保护单（STOP_MARKET + TAKE_PROFIT_MARKET）。
    该接口用于“手动补挂”，不依赖 trading_system 的监控任务。
    """
    # 从 accounts 表读取账号私有API密钥
    account_id_int = int(account_id or 1)
    api_key, api_secret, use_testnet, status = Account.get_credentials(account_id_int)
    if (not api_key or not api_secret) and status == "active":
        logger.error(f"[account_id={account_id_int}] API密钥未配置")
        raise HTTPException(status_code=400, detail=f"API密钥未配置（account_id={account_id_int}）")

    # 导入交易系统的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)
    await client.connect()

    try:
        # 1) 获取当前持仓（需要知道方向）
        raw_positions = await client.client.futures_position_information(symbol=symbol)
        nonzero = []
        for p in raw_positions or []:
            try:
                amt = float(p.get("positionAmt", 0) or 0)
                if amt != 0:
                    nonzero.append((amt, p))
            except Exception:
                continue
        if not nonzero:
            raise HTTPException(status_code=400, detail=f"{symbol} 当前无持仓，无法补挂止盈止损")

        # 2) 获取持仓模式
        dual_side = None
        try:
            mode_res = await client.client.futures_get_position_mode()
            if isinstance(mode_res, dict):
                dual_side = bool(mode_res.get("dualSidePosition"))
        except Exception:
            dual_side = None

        # 3) 取净持仓（单向）或第一条非零腿（对冲/兜底）
        amt, p0 = nonzero[0]
        net_amt = sum([a for a, _ in nonzero])
        if dual_side is False:
            amt = net_amt
        side = "BUY" if amt > 0 else "SELL"

        mark_price = None
        try:
            mark_price = float(p0.get("markPrice", 0) or 0) or None
        except Exception:
            mark_price = None

        # 4) 止损/止盈价：优先从数据库 open trade 取；无 DB 记录时用币安持仓 + 配置比例计算（支持手动开仓补挂）
        from database.models import Trade
        open_trades = Trade.get_by_symbol(symbol, status='open', account_id=account_id) or []
        sl = None
        tp = None
        entry_price = float(p0.get("entryPrice", 0) or 0)
        qty = abs(float(amt))
        lv = float(p0.get("leverage", 0) or 0) or 10.0

        def _ratio(v, default):
            try:
                x = float(v)
                if x > 1:
                    x = x / 100.0
                if x < 0:
                    x = default
                return x
            except Exception:
                return default

        if open_trades:
            try:
                open_trades.sort(key=lambda x: int(x.get("id", 0) or 0), reverse=True)
            except Exception:
                pass
            trade = open_trades[0]
            sl = trade.get("stop_loss_price")
            tp = trade.get("take_profit_2") or trade.get("take_profit_price") or trade.get("take_profit_1")
            try:
                sl = float(sl) if sl is not None else None
            except Exception:
                sl = None
            try:
                tp = float(tp) if tp is not None else None
            except Exception:
                tp = None
            if entry_price <= 0 or qty <= 0:
                entry_price = float(trade.get("entry_price") or 0)
                qty = float(trade.get("quantity") or 0)
                lv = float(trade.get("leverage") or 0) or lv

        # 无 DB 记录或 DB 中 sl/tp 为空：用币安持仓的 entry/quantity/leverage + 配置比例计算
        if not sl or not tp:
            try:
                if entry_price <= 0 or qty <= 0 or lv <= 0:
                    raise ValueError("entry_price/quantity/leverage invalid")
                sl_pct = _ratio(TradingConfig.get_value("STOP_LOSS_PERCENT", 0.05), 0.05)
                tp_pct = _ratio(TradingConfig.get_value("TAKE_PROFIT_PERCENT", 0.15), 0.15)
                notional = entry_price * qty
                margin = notional / lv
                sl_amount = margin * sl_pct
                tp_amount = margin * tp_pct
                if side == "BUY":
                    sl = entry_price - (sl_amount / qty)
                    tp = entry_price + (tp_amount / qty)
                else:
                    sl = entry_price + (sl_amount / qty)
                    tp = entry_price - (tp_amount / qty)
                if not sl or not tp or sl <= 0 or tp <= 0:
                    raise ValueError("computed sl/tp invalid")
            except Exception as e:
                raise HTTPException(
                    status_code=400,
                    detail=f"{symbol} 无法确定止损止盈价（无 DB 记录且计算失败: {e}），请检查配置 STOP_LOSS_PERCENT/TAKE_PROFIT_PERCENT"
                )

        # 5) 取消旧的保护单（Algo 条件单），避免重复
        try:
            await client.cancel_open_algo_orders_by_order_types(
                symbol, {"STOP_MARKET", "TAKE_PROFIT_MARKET", "TRAILING_STOP_MARKET"}
            )
        except Exception:
            pass

        # 6) 下保护单（closePosition=True）
        symbol_info = await client.get_symbol_info(symbol)
        # 使用 trading_system/binance_client 的格式化方法（如果不存在则回退简单格式）
        try:
            fmt_price = BinanceClient._format_price_str_with_rounding  # type: ignore[attr-defined]
        except Exception:
            fmt_price = None

        def _fmt(price: float, rounding_mode: str) -> str:
            if fmt_price:
                return fmt_price(price, symbol_info, rounding_mode)  # type: ignore[misc]
            return str(round(float(price), int(symbol_info.get("pricePrecision", 8) or 8) if symbol_info else 8))

        # 触发价避免“立即触发”
        cp = float(mark_price) if mark_price else None
        tick = float(symbol_info.get("tickSize", 0) or 0) if symbol_info else 0.0
        pp = int(symbol_info.get("pricePrecision", 8) or 8) if symbol_info else 8
        min_step = tick if tick and tick > 0 else (10 ** (-pp) if pp and pp > 0 else 1e-8)

        sl_price = float(sl)
        tp_price = float(tp)
        if cp and cp > 0:
            # stop
            if side == "BUY" and sl_price >= cp:
                sl_price = max(0.0, cp - min_step)
            if side == "SELL" and sl_price <= cp:
                sl_price = cp + min_step
            # tp
            if side == "BUY" and tp_price <= cp:
                tp_price = cp + min_step
            if side == "SELL" and tp_price >= cp:
                tp_price = max(0.0, cp - min_step)

        # rounding：止损 long 用 UP，short 用 DOWN；止盈 long 用 DOWN，short 用 UP
        sl_round = "UP" if side == "BUY" else "DOWN"
        tp_round = "DOWN" if side == "BUY" else "UP"

        close_side = "SELL" if side == "BUY" else "BUY"
        # Algo 条件单使用 triggerPrice（不是 stopPrice）
        sl_params = {
            "algoType": "CONDITIONAL",
            "symbol": symbol,
            "side": close_side,
            "type": "STOP_MARKET",
            "triggerPrice": _fmt(sl_price, sl_round),
            "closePosition": True,
            "workingType": "MARK_PRICE",
        }
        tp_params = {
            "algoType": "CONDITIONAL",
            "symbol": symbol,
            "side": close_side,
            "type": "TAKE_PROFIT_MARKET",
            "triggerPrice": _fmt(tp_price, tp_round),
            "closePosition": True,
            "workingType": "MARK_PRICE",
        }
        if dual_side is True:
            sl_params["positionSide"] = "LONG" if side == "BUY" else "SHORT"
            tp_params["positionSide"] = "LONG" if side == "BUY" else "SHORT"

        sl_order = await client.futures_create_algo_order(sl_params)
        tp_order = await client.futures_create_algo_order(tp_params)

        # 再查一次未成交委托，确认是否真的挂上（并用于前端展示/排查）
        open_orders = []
        try:
            oo = await client.futures_get_open_algo_orders(symbol=symbol, algo_type="CONDITIONAL")
            if isinstance(oo, list):
                for o in oo:
                    try:
                        if not isinstance(o, dict):
                            continue
                        otype2 = str(o.get("orderType") or o.get("type") or "").upper()
                        if otype2 in {"STOP_MARKET", "TAKE_PROFIT_MARKET", "TRAILING_STOP_MARKET", "STOP", "TAKE_PROFIT"}:
                            open_orders.append(
                                {
                                    "algoId": o.get("algoId"),
                                    "orderType": otype2,
                                    "side": o.get("side"),
                                    "triggerPrice": o.get("triggerPrice"),
                                    "workingType": o.get("workingType"),
                                    "positionSide": o.get("positionSide"),
                                    "closePosition": o.get("closePosition"),
                                    "algoStatus": o.get("algoStatus"),
                                    "updateTime": o.get("updateTime"),
                                }
                            )
                    except Exception:
                        continue
        except Exception:
            open_orders = []

        return {
            "symbol": symbol,
            "position_side": side,
            "dual_side_position": dual_side,
            "stop_loss_price": sl_price,
            "take_profit_price": tp_price,
            "orders": {
                "stop_market": sl_order,
                "take_profit_market": tp_order,
            },
            "open_protection_orders": open_orders,
            "ui_hint": "在币安【U本位合约】里，这类 STOP/TP 通常显示在【条件单/止盈止损/计划委托】而不一定在普通【当前委托(限价)】列表。",
        }
    finally:
        await client.disconnect()


@router.post("/positions/{symbol}/sltp/ensure")
async def ensure_position_sltp(symbol: str, account_id: int = Depends(get_account_id)):
    """
    手动补挂该 symbol 的止盈止损保护单（币安侧可见）。
    """
    try:
        return await _ensure_exchange_sltp_for_symbol(symbol, account_id=int(account_id))
    except HTTPException:
        raise
    except Exception as e:
        msg = str(e) or repr(e) or "unknown error"
        logger.error(f"{symbol} 补挂止盈止损失败: {msg}", exc_info=True)
        raise HTTPException(status_code=500, detail=f"补挂止盈止损失败: {msg}")


@router.post("/positions/sltp/ensure-all")
async def ensure_all_positions_sltp(
    limit: int = Query(50, ge=1, le=200, description="最多处理多少个持仓symbol"),
    account_id: int = Depends(get_account_id),
):
    """
    批量补挂当前所有持仓的止盈止损保护单。
    """
    # 先拿当前持仓symbol列表
    api_key, api_secret, use_testnet, status = Account.get_credentials(account_id)
    if (not api_key or not api_secret) and status == "active":
        logger.error(f"[account_id={account_id}] API密钥未配置")
        raise HTTPException(status_code=400, detail=f"API密钥未配置（account_id={account_id}）")

    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)
    await client.connect()
    try:
        positions = await client.get_open_positions()
        symbols = [p["symbol"] for p in (positions or []) if float(p.get("positionAmt", 0) or 0) != 0]
    finally:
        await client.disconnect()

    symbols = symbols[: int(limit or 50)]
    results = []
    errors = []
    for sym in symbols:
        try:
            res = await _ensure_exchange_sltp_for_symbol(sym, account_id=account_id)
            results.append(
                {
                    "symbol": sym,
                    "ok": True,
                    "orders": res.get("orders"),
                    "open_protection_orders": res.get("open_protection_orders"),
                }
            )
        except HTTPException as he:
            errors.append(
                {
                    "symbol": sym,
                    "ok": False,
                    "status_code": getattr(he, "status_code", None),
                    "detail": getattr(he, "detail", None),
                }
            )
        except Exception as e:
            msg = str(e) or repr(e) or "unknown error"
            errors.append({"symbol": sym, "ok": False, "error": msg})

    return {
        "total": len(symbols),
        "ok": len([r for r in results if r.get("ok")]),
        "failed": len(errors),
        "results": results,
        "errors": errors,
    }


async def get_realtime_account_data(account_id: int = 1):
    """从币安API实时获取账户数据"""
    logger.info("=" * 60)
    logger.info("开始获取实时账户数据")
    logger.info("=" * 60)
    
    try:
        # 从 accounts 表读取账号私有API密钥
        logger.info(f"步骤1: 从accounts读取API配置... (account_id={account_id})")
        logger.info(f"  - 请求的 account_id: {account_id}")
        api_key, api_secret, use_testnet, status = Account.get_credentials(account_id)
        logger.info(f"  - 获取到的 account_id 状态: {status}")
        
        logger.info(f"  - API密钥存在: {bool(api_key)}")
        if api_key:
            logger.info(f"  - API密钥长度: {len(api_key)} 字符")
        else:
            logger.warning("  - API密钥为空!")
        
        logger.info(f"  - API密钥Secret存在: {bool(api_secret)}")
        if api_secret:
            logger.info(f"  - API密钥Secret长度: {len(api_secret)} 字符")
        else:
            logger.warning("  - API密钥Secret为空!")
        
        logger.info(f"  - 使用测试网: {use_testnet}")
        
        if not api_key or not api_secret:
            error_msg = f"API密钥未配置（account_id={account_id}），请在配置界面设置该账号的BINANCE_API_KEY和BINANCE_API_SECRET"
            logger.error(f"[account_id={account_id}] API密钥未配置")
            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实例...")
        logger.info(f"  - 使用的 account_id: {account_id}")
        logger.info(f"  - API Key 前4位: {api_key[:4] if api_key and len(api_key) >= 4 else 'N/A'}...")
        logger.info(f"  - API Key 后4位: ...{api_key[-4:] if api_key and len(api_key) >= 4 else 'N/A'}")
        logger.info(f"  - API Secret 前4位: {api_secret[:4] if api_secret and len(api_secret) >= 4 else 'N/A'}...")
        logger.info(f"  - API Secret 后4位: ...{api_secret[-4:] if api_secret and len(api_secret) >= 4 else 'N/A'}")
        logger.info(f"  - testnet: {use_testnet}")
        
        # 确保传递了正确的 api_key 和 api_secret，避免 BinanceClient 从 config 读取
        if not api_key or not api_secret:
            error_msg = f"API密钥为空 (account_id={account_id})，无法创建BinanceClient"
            logger.error(f"  ✗ {error_msg}")
            raise HTTPException(status_code=400, detail=error_msg)
        
        client = BinanceClient(
            api_key=api_key,  # 明确传递，避免从 config 读取
            api_secret=api_secret,  # 明确传递，避免从 config 读取
            testnet=use_testnet
        )
        logger.info(f"  ✓ 客户端创建成功 (testnet={use_testnet}, account_id={account_id})")
        
        # 连接币安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

        # 读取持仓模式（单向/对冲），用于前端“重点说明/自检”
        dual_side_position = None
        position_mode = None
        try:
            mode_res = await client.client.futures_get_position_mode()
            if isinstance(mode_res, dict):
                dual_side_position = bool(mode_res.get("dualSidePosition"))
                position_mode = "hedge" if dual_side_position else "one_way"
        except Exception as e:
            logger.warning(f"读取持仓模式失败（将显示为未知）: {e}")
        
        # 获取账户余额
        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：历史上这里代表“名义仓位价值(notional)”（按标记价）
        total_position_value = 0
        # total_margin_value：更贴近风控配置语义（保证金占用）
        total_margin_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

            # 保证金占用（粗略口径）：名义/杠杆（币安页面的展示会更复杂，但这个口径与 MAX_TOTAL_POSITION_PERCENT 对齐）
            try:
                lv = float(pos.get('leverage', 0) or 0)
                if lv <= 0:
                    lv = 1.0
            except Exception:
                lv = 1.0
            total_margin_value += (position_value / lv)
            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_margin_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}")
        
        # 构建返回结果
        # 注意：币安合约账户的余额字段（根据官方文档）：
        # - walletBalance: 钱包余额（不包括未实现盈亏，只反映已实现的盈亏、转账、手续费等）
        # - marginBalance: 保证金余额（钱包余额 + 未实现盈亏），这是账户的总权益，用户看到的"总余额"
        # - availableBalance: 可用余额（可用于开仓的余额）
        # 这里使用 marginBalance 作为 total_balance，因为这才是用户看到的"总余额"（包括未实现盈亏）
        wallet_balance = balance.get('walletBalance') if balance and 'walletBalance' in balance else balance.get('total', 0) if balance else 0
        available_balance = balance.get('availableBalance') if balance and 'availableBalance' in balance else balance.get('available', 0) if balance else 0
        margin_balance = balance.get('marginBalance') if balance and 'marginBalance' in balance else balance.get('margin', 0) if balance else 0
        unrealized_profit = balance.get('unrealizedProfit', 0) if balance else 0
        
        # 如果没有 marginBalance，尝试从 total 字段获取（向后兼容）
        if margin_balance == 0 and balance and 'total' in balance:
            margin_balance = balance.get('total', 0)
        
        logger.info(f"构建返回结果:")
        logger.info(f"  - wallet_balance (钱包余额，不包括未实现盈亏): {wallet_balance}")
        logger.info(f"  - margin_balance (保证金余额，总权益，包括未实现盈亏): {margin_balance}")
        logger.info(f"  - available_balance (可用余额): {available_balance}")
        logger.info(f"  - unrealized_profit (未实现盈亏): {unrealized_profit}")
        
        result = {
            "total_balance": margin_balance,  # 使用保证金余额作为总余额（包括未实现盈亏），这是用户看到的"总余额"
            "available_balance": available_balance,
            "margin_balance": margin_balance,  # 添加保证金余额字段
            "wallet_balance": wallet_balance,  # 添加钱包余额字段（不包括未实现盈亏）
            "unrealized_profit": unrealized_profit,  # 添加未实现盈亏字段
            # 名义仓位（按标记价汇总）
            "total_position_value": total_position_value,
            # 保证金占用（名义/杠杆汇总）
            "total_margin_value": total_margin_value,
            "total_pnl": total_pnl,
            "open_positions": open_positions_count,
            # 账户持仓模式（重点：建议使用 one_way）
            "position_mode": position_mode,
            "dual_side_position": dual_side_position,
        }
        
        logger.info("=" * 60)
        logger.info(f"账户数据获取成功! (account_id={account_id})")
        logger.info(f"最终结果 - total_balance={result.get('total_balance', 'N/A')}, available_balance={result.get('available_balance', 'N/A')}, open_positions={result.get('open_positions', 'N/A')}")
        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(account_id: int = Depends(get_account_id)):
    """获取实时账户数据"""
    return await get_realtime_account_data(account_id=account_id)


async def fetch_live_positions_pnl(account_id: int):
    """
    获取指定账号的实时持仓盈亏（仅 mark_price / pnl / pnl_percent），供仪表板合并用。
    失败时返回空列表，不抛异常。
    """
    client = None
    try:
        api_key, api_secret, use_testnet, status = Account.get_credentials(account_id)
        if not api_key or not api_secret:
            return []
        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)
        await client.connect()
        positions = await client.get_open_positions()
        result = []
        for pos in positions:
            amt = float(pos.get('positionAmt', 0))
            if amt == 0:
                continue
            entry_price = float(pos.get('entryPrice', 0))
            mark_price = float(pos.get('markPrice', 0)) or entry_price
            unrealized_pnl = float(pos.get('unRealizedProfit', 0))
            leverage = max(1, float(pos.get('leverage', 1)))
            notional = abs(amt) * mark_price
            margin = notional / leverage
            pnl_percent = (unrealized_pnl / margin * 100) if margin > 0 else 0
            result.append({
                "symbol": pos.get("symbol"),
                "mark_price": mark_price,
                "pnl": unrealized_pnl,
                "pnl_percent": pnl_percent,
            })
        return result
    except Exception as e:
        logger.debug(f"fetch_live_positions_pnl(account_id={account_id}) 失败: {e}")
        return []
    finally:
        try:
            if client is not None:
                await client.disconnect()
        except Exception:
            pass


async def fetch_realtime_positions(account_id: int):
    """
    获取指定账号的「币安实时持仓」列表（与仪表板/GET /positions 一致）。
    每条持仓会尝试关联本账号下的 DB 记录（开仓时间、止损止盈、entry_order_id 等）。
    失败时返回 []，不抛异常，便于仪表板回退到 DB 列表。
    """
    client = None
    try:
        api_key, api_secret, use_testnet, status = Account.get_credentials(account_id)
        if not api_key or not api_secret:
            logger.debug(f"fetch_realtime_positions(account_id={account_id}): 无 API 密钥，返回空列表")
            return []

        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)
        await client.connect()
        positions = await client.get_open_positions()

        open_orders_map = {}
        try:
            position_symbols = [p.get('symbol') for p in positions if float(p.get('positionAmt', 0)) != 0]
            if position_symbols:
                async def fetch_both_orders(symbol):
                    try:
                        t1 = client.get_open_orders(symbol)
                        t2 = client.futures_get_open_algo_orders(symbol, algo_type="CONDITIONAL")
                        res = await asyncio.gather(t1, t2, return_exceptions=True)
                        orders = []
                        if isinstance(res[0], list):
                            orders.extend(res[0])
                        if isinstance(res[1], list):
                            for algo in res[1]:
                                orders.append({
                                    'orderId': algo.get('algoId'),
                                    'type': algo.get('orderType'),
                                    'side': algo.get('side'),
                                    'stopPrice': algo.get('triggerPrice'),
                                    'price': 0,
                                    'origType': algo.get('algoType'),
                                    'reduceOnly': algo.get('reduceOnly'),
                                    'status': 'NEW',
                                    '_is_algo': True
                                })
                        return orders
                    except Exception as e:
                        logger.debug(f"获取 {symbol} 订单失败: {e}")
                        return []

                tasks = [fetch_both_orders(sym) for sym in position_symbols]
                results = await asyncio.gather(*tasks, return_exceptions=True)
                for sym, orders in zip(position_symbols, results):
                    if isinstance(orders, list):
                        conditional_orders = []
                        for o in orders:
                            o_type = o.get('type')
                            if o_type in ['STOP_MARKET', 'TAKE_PROFIT_MARKET', 'STOP', 'TAKE_PROFIT']:
                                conditional_orders.append({
                                    'orderId': o.get('orderId'),
                                    'type': o_type,
                                    'side': o.get('side'),
                                    'stopPrice': float(o.get('stopPrice', 0)),
                                    'price': float(o.get('price', 0)),
                                    'origType': o.get('origType'),
                                    'reduceOnly': o.get('reduceOnly'),
                                    'status': o.get('status')
                                })
                        if conditional_orders:
                            open_orders_map[sym] = conditional_orders
        except Exception as e:
            logger.debug(f"批量获取挂单失败: {e}")

        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)) or entry_price
            unrealized_pnl = float(pos.get('unRealizedProfit', 0))
            entry_value_usdt = abs(position_amt) * entry_price
            leverage = max(1.0, float(pos.get('leverage', 1)))
            notional_usdt_live = abs(position_amt) * mark_price
            margin_usdt_live = notional_usdt_live / leverage
            pnl_percent = (unrealized_pnl / margin_usdt_live * 100) if margin_usdt_live > 0 else 0

            entry_time = None
            created_at = 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
            entry_order_id = None
            entry_order_type = None
            id = None
            update_time_ms = pos.get('updateTime')  # 币安持仓最后更新时间(ms)，无 DB 时可用于展示
            try:
                from database.models import Trade
                db_trades = Trade.get_by_symbol(pos.get('symbol'), status='open', account_id=account_id)
                if db_trades:
                    matched = None
                    for db_trade in db_trades:
                        try:
                            if abs(float(db_trade.get('entry_price', 0)) - entry_price) < 0.01:
                                matched = db_trade
                                break
                        except Exception:
                            continue
                    if matched is None:
                        matched = db_trades[0]
                    entry_time = matched.get('entry_time')
                    # 创建时间：兼容 DB 列名 created_at / create_at
                    created_at = matched.get('created_at') if matched.get('created_at') is not None else matched.get('create_at')
                    stop_loss_price = matched.get('stop_loss_price')
                    take_profit_price = matched.get('take_profit_price')
                    take_profit_1 = matched.get('take_profit_1')
                    take_profit_2 = matched.get('take_profit_2')
                    atr_value = matched.get('atr')
                    db_margin_usdt = matched.get('margin_usdt')
                    db_notional_usdt = matched.get('notional_usdt')
                    entry_order_id = matched.get('entry_order_id')
                    id = matched.get('id')
            except Exception as e:
                logger.debug(f"获取数据库信息失败: {e}")
            
            if entry_order_id:
                try:
                    info = await client.client.futures_get_order(symbol=pos.get('symbol'), orderId=int(entry_order_id))
                    if isinstance(info, dict):
                        entry_order_type = info.get("type")
                        # 用开仓订单的下单时间补全开仓时间（当 DB 未提供时）
                        order_time_ms = info.get("time")
                        if (entry_time is None or created_at is None) and order_time_ms:
                            order_time_sec = int(order_time_ms) // 1000
                            if entry_time is None:
                                entry_time = order_time_sec
                            if created_at is None:
                                created_at = order_time_sec
                except Exception:
                    entry_order_type = None

            symbol_orders = open_orders_map.get(pos.get('symbol'), [])
            # 若 DB 未提供止损/止盈，从交易所 open_orders 反填，避免持仓记录显示“无 SL/TP”而实际有挂单
            if symbol_orders and (stop_loss_price is None or take_profit_price is None or take_profit_1 is None):
                is_long = position_amt > 0
                sl_prices = []
                tp_prices = []
                for o in symbol_orders:
                    if not o.get('reduceOnly'):
                        continue
                    t = o.get('type')
                    sp = o.get('stopPrice')
                    try:
                        sp = float(sp) if sp is not None else None
                    except (TypeError, ValueError):
                        sp = None
                    if sp is None:
                        continue
                    if t == 'STOP_MARKET':
                        sl_prices.append(sp)
                    elif t in ('TAKE_PROFIT_MARKET', 'TAKE_PROFIT'):
                        tp_prices.append(sp)
                if sl_prices and stop_loss_price is None:
                    stop_loss_price = sl_prices[0]
                if tp_prices:
                    tp_prices.sort(key=lambda p: abs(p - entry_price))
                    if take_profit_1 is None:
                        take_profit_1 = tp_prices[0]
                    if len(tp_prices) > 1 and take_profit_2 is None:
                        take_profit_2 = tp_prices[1]
                    if take_profit_price is None:
                        take_profit_price = take_profit_1
            
            formatted_positions.append({
                "id": id,
                "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,
                "notional_usdt": notional_usdt_live,
                "margin_usdt": margin_usdt_live,
                "original_notional_usdt": db_notional_usdt,
                "original_margin_usdt": db_margin_usdt,
                "mark_price": mark_price,
                "pnl": unrealized_pnl,
                "pnl_percent": pnl_percent,
                "leverage": int(pos.get('leverage', 1)),
                "entry_time": entry_time,
                "created_at": created_at,
                "update_time": int(update_time_ms) // 1000 if update_time_ms else None,
                "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,
                "entry_order_id": entry_order_id,
                "entry_order_type": entry_order_type,
                "open_orders": symbol_orders,
            })
        return formatted_positions
    except Exception as e:
        logger.warning(f"fetch_realtime_positions(account_id={account_id}) 失败: {e}", exc_info=True)
        return []
    finally:
        try:
            if client is not None:
                await client.disconnect()
        except Exception:
            pass


@router.get("/positions")
async def get_realtime_positions(account_id: int = Depends(get_account_id)):
    """获取实时持仓数据（币安实际持仓，并关联本账号 DB 记录）"""
    api_key, api_secret, _, _ = Account.get_credentials(account_id)
    if not api_key or not api_secret:
        raise HTTPException(status_code=400, detail=f"API密钥未配置（account_id={account_id}）")
    result = await fetch_realtime_positions(account_id)
    if result is None:
        raise HTTPException(status_code=500, detail="获取持仓数据失败")
    return result


@router.post("/positions/{symbol}/close")
async def close_position(symbol: str, account_id: int = Depends(get_account_id)):
    """手动平仓指定交易对的持仓"""
    try:
        logger.info(f"=" * 60)
        logger.info(f"收到平仓请求: {symbol}")
        logger.info(f"=" * 60)
        
        api_key, api_secret, use_testnet, status = Account.get_credentials(account_id)
        
        if (not api_key or not api_secret) and status == "active":
            error_msg = f"API密钥未配置（account_id={account_id}）"
            logger.warning(f"[account_id={account_id}] {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:
            # 检查币安是否有持仓（使用原始 position_information，确保能拿到 positionSide 以处理 -4061）
            logger.info(f"检查 {symbol} 在币安的持仓状态...")

            # 读取持仓模式：dualSidePosition=True => 对冲模式（必须传 positionSide=LONG/SHORT）
            dual_side = None
            try:
                mode_res = await client.client.futures_get_position_mode()
                if isinstance(mode_res, dict):
                    dual_side = bool(mode_res.get("dualSidePosition"))
            except Exception as e:
                logger.warning(f"读取持仓模式失败（将按单向模式兜底）: {e}")
                dual_side = None

            raw_positions = await client.client.futures_position_information(symbol=symbol)
            nonzero_positions = []
            for p in raw_positions or []:
                try:
                    amt = float(p.get("positionAmt", 0))
                except Exception:
                    continue
                if abs(amt) > 0:
                    nonzero_positions.append((amt, p))

            # 兼容旧逻辑：如果原始接口异常，回退到封装方法
            if not nonzero_positions:
                try:
                    positions = await client.get_open_positions()
                    position = next((p for p in positions if p['symbol'] == symbol and float(p['positionAmt']) != 0), None)
                    if position:
                        nonzero_positions = [(float(position["positionAmt"]), {"positionAmt": position["positionAmt"]})]
                except Exception:
                    nonzero_positions = []
            
            if not nonzero_positions:
                logger.warning(f"⚠ {symbol} 币安账户中没有持仓，可能已被平仓")
                # 检查数据库中是否有未平仓的记录，如果有则更新（仅当前账号）
                open_trades = Trade.get_by_symbol(symbol, status='open', account_id=account_id)
                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"
                }
            
            # 获取交易对精度信息，调整数量精度（平仓不要向上补 minQty，避免超过持仓数量）
            symbol_info = None
            try:
                symbol_info = await client.get_symbol_info(symbol)
            except Exception:
                symbol_info = None

            def _adjust_close_qty(qty: float) -> float:
                if qty is None:
                    return 0.0
                q = float(qty)
                if not symbol_info:
                    return q
                    quantity_precision = symbol_info.get('quantityPrecision', 8)
                step_size = float(symbol_info.get('stepSize', 0) or 0)
                if step_size and step_size > 0:
                    # 向下取整，避免超过持仓
                    q = float(int(q / step_size)) * step_size
                else:
                    q = round(q, quantity_precision)
                q = round(q, quantity_precision)
                return q

            # 组装平仓订单（对冲模式可能同币种有 LONG/SHORT 两个仓位，这里一并平掉）
            orders = []
            order_ids = []

            # 如果 dual_side 无法读取，按 raw_positions 是否包含 positionSide 来推断
            if dual_side is None:
                if any(isinstance(p, dict) and (p.get("positionSide") in ("LONG", "SHORT")) for _, p in nonzero_positions):
                    dual_side = True
                else:
                    dual_side = False

            logger.info(f"{symbol} 持仓模式: {'HEDGE(对冲)' if dual_side else 'ONE-WAY(单向)'}")

            # 构造待平仓列表：[(positionSide, amt)]
            to_close = []
            if dual_side:
                for amt, p in nonzero_positions:
                    ps = (p.get("positionSide") or "").upper()
                    if ps not in ("LONG", "SHORT"):
                        ps = "LONG" if amt > 0 else "SHORT"
                    to_close.append((ps, amt))
            else:
                # 单向模式只应存在一个净仓位；如果有多个，按合计处理
                net_amt = sum([amt for amt, _ in nonzero_positions])
                if abs(net_amt) > 0:
                    to_close.append(("BOTH", net_amt))

            logger.info(f"✓ 币安账户中 {symbol} 待平仓: {to_close}")

            for ps, amt in to_close:
                side = 'SELL' if float(amt) > 0 else 'BUY'
                quantity = abs(float(amt))
                quantity = _adjust_close_qty(quantity)
                if quantity <= 0:
                    logger.warning(f"{symbol} 平仓数量调整后为0，跳过该仓位: positionSide={ps}, amt={amt}")
                    continue

                order_params = {
                    "symbol": symbol,
                    "side": side,
                    "type": "MARKET",
                    "quantity": quantity,
                }
                # 对冲模式必须传 positionSide=LONG/SHORT；并且某些账户会 -1106，因此这里不再传 reduceOnly
                if dual_side and ps in ("LONG", "SHORT"):
                    order_params["positionSide"] = ps
                else:
                    # 单向模式用 reduceOnly 防止反向开仓
                    order_params["reduceOnly"] = True

                logger.info(
                    f"开始执行平仓下单: {symbol} side={side} qty={quantity} "
                    f"positionSide={order_params.get('positionSide')} reduceOnly={order_params.get('reduceOnly')}"
                )
                try:
                    order = await client.client.futures_create_order(**order_params)
                    if not order:
                        raise RuntimeError("币安API返回 None")
                    orders.append(order)
                    oid = order.get("orderId")
                    if oid:
                        order_ids.append(oid)
                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)
            
            if not orders:
                raise HTTPException(status_code=400, detail=f"{symbol} 无可平仓的有效仓位（数量调整后为0或无持仓）")

            logger.info(f"✓ {symbol} 平仓订单已提交: {order_ids}")
            
            # 等待订单成交，获取实际成交价格
            import asyncio
            await asyncio.sleep(1)
            
            # 获取订单详情（可能多个订单，按订单号分别取价）
            exit_prices = {}
            exit_commissions = {}
            exit_realized_pnls = {}
            exit_commission_assets = {}

            # 新增：获取最近成交记录以计算佣金和实际盈亏
            try:
                # 等待一小段时间确保成交记录已生成
                await asyncio.sleep(1)
                recent_trades = await client.get_recent_trades(symbol, limit=20)
            except Exception as e:
                logger.warning(f"获取最近成交记录失败: {e}")
                recent_trades = []

            for oid in order_ids:
                try:
                    # 1. 获取价格
                    order_info = await client.client.futures_get_order(symbol=symbol, orderId=oid)
                    if order_info:
                        p = float(order_info.get('avgPrice', 0)) or float(order_info.get('price', 0))
                        if p <= 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:
                                p = total_value / total_qty
                        if p > 0:
                            exit_prices[oid] = p

                    # 2. 计算佣金和实际盈亏（从 recent_trades 匹配）
                    related_trades = [t for t in recent_trades if str(t.get('orderId')) == str(oid)]
                    if related_trades:
                        total_realized_pnl = 0.0
                        total_commission = 0.0
                        commission_assets = set()
                        for t in related_trades:
                            total_realized_pnl += float(t.get('realizedPnl', 0))
                            total_commission += float(t.get('commission', 0))
                            commission_assets.add(t.get('commissionAsset'))

                        exit_realized_pnls[oid] = total_realized_pnl
                        exit_commissions[oid] = total_commission
                        exit_commission_assets[oid] = "/".join(commission_assets) if commission_assets else None
                except Exception as e:
                    logger.warning(f"获取订单详情失败 (orderId={oid}): {e}")
            
            # 兜底：如果无法获取订单价格，使用当前价格
            fallback_exit_price = None
            try:
                ticker = await client.get_ticker_24h(symbol)
                fallback_exit_price = float(ticker['price']) if ticker else None
            except Exception:
                fallback_exit_price = None
            
            # 更新数据库记录
            open_trades = Trade.get_by_symbol(symbol, status='open', account_id=account_id)
            if open_trades:
                # 对冲模式可能有多条 trade（BUY/LONG 和 SELL/SHORT），尽量按方向匹配订单更新
                used_order_ids = set()
                for trade in open_trades:
                    try:
                        entry_price = float(trade['entry_price'])
                        trade_quantity = float(trade['quantity'])
                    except Exception:
                        continue

                    # 选择一个未使用的 orderId（如果只有一个，就复用）
                    chosen_oid = None
                    for oid in order_ids:
                        if oid not in used_order_ids:
                            chosen_oid = oid
                            break
                    if chosen_oid is None and order_ids:
                        chosen_oid = order_ids[0]
                    if chosen_oid:
                        used_order_ids.add(chosen_oid)

                    exit_price = exit_prices.get(chosen_oid) if chosen_oid else None
                    if not exit_price:
                        exit_price = fallback_exit_price or entry_price

                    # 计算盈亏（数据库侧依旧按名义盈亏；收益率展示用保证金口径在前端/统计里另算）
                    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=chosen_oid,
                        realized_pnl=exit_realized_pnls.get(chosen_oid),
                        commission=exit_commissions.get(chosen_oid),
                        commission_asset=exit_commission_assets.get(chosen_oid)
                    )
                    logger.info(f"✓ 已更新数据库记录 trade_id={trade['id']} order_id={chosen_oid} (盈亏: {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/close-all")
async def close_all_positions(account_id: int = Depends(get_account_id)):
    """一键全平：平仓所有持仓"""
    try:
        logger.info("=" * 60)
        logger.info("收到一键全平请求")
        logger.info("=" * 60)
        
        api_key, api_secret, use_testnet, status = Account.get_credentials(account_id)
        
        if (not api_key or not api_secret) and status == "active":
            error_msg = f"API密钥未配置（account_id={account_id}）"
            logger.warning(f"[account_id={account_id}] {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:
            # 获取所有持仓
            positions = await client.get_open_positions()
            if not positions:
                logger.info("当前没有持仓")
                return {
                    "message": "当前没有持仓",
                    "closed": 0,
                    "failed": 0,
                    "results": []
                }
            
            logger.info(f"发现 {len(positions)} 个持仓，开始逐一平仓...")
            
            results = []
            closed_count = 0
            failed_count = 0
            
            for position in positions:
                symbol = position.get('symbol')
                position_amt = float(position.get('positionAmt', 0))
                
                if abs(position_amt) <= 0:
                    continue
                
                try:
                    logger.info(f"开始平仓 {symbol} (数量: {position_amt})...")
                    
                    # 确定平仓方向
                    side = 'SELL' if position_amt > 0 else 'BUY'
                    
                    # 使用市价单平仓
                    order = await client.place_order(
                        symbol=symbol,
                        side=side,
                        order_type='MARKET',
                        quantity=abs(position_amt),
                        reduce_only=True
                    )
                    
                    if order and order.get('orderId'):
                        logger.info(f"✓ {symbol} 平仓订单已提交: {order.get('orderId')}")
                        
                        # 获取成交价格
                        exit_price = float(order.get('avgPrice', 0)) or float(order.get('price', 0))
                        if not exit_price:
                            # 如果订单中没有价格，获取当前价格
                            ticker = await client.get_ticker_24h(symbol)
                            exit_price = float(ticker['price']) if ticker else 0
                        
                        # 更新数据库记录（仅当前账号）
                        open_trades = Trade.get_by_symbol(symbol, status='open', account_id=account_id)
                        for trade in open_trades:
                            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=order.get('orderId')
                            )
                            logger.info(f"✓ 已更新数据库记录 trade_id={trade['id']} (盈亏: {pnl:.2f} USDT)")
                        
                        closed_count += 1
                        results.append({
                            "symbol": symbol,
                            "status": "success",
                            "order_id": order.get('orderId'),
                            "message": f"{symbol} 平仓成功"
                        })
                    else:
                        logger.warning(f"⚠ {symbol} 平仓订单提交失败")
                        failed_count += 1
                        results.append({
                            "symbol": symbol,
                            "status": "failed",
                            "message": f"{symbol} 平仓失败: 订单未提交"
                        })
                        
                except Exception as e:
                    logger.error(f"❌ {symbol} 平仓失败: {e}")
                    failed_count += 1
                    results.append({
                        "symbol": symbol,
                        "status": "failed",
                        "message": f"{symbol} 平仓失败: {str(e)}"
                    })
            
            logger.info(f"一键全平完成: 成功 {closed_count} / 失败 {failed_count}")
            return {
                "message": f"一键全平完成: 成功 {closed_count} / 失败 {failed_count}",
                "closed": closed_count,
                "failed": failed_count,
                "results": results
            }
            
        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/{symbol}/open")
async def open_position_from_recommendation(
    symbol: str,
    entry_price: float = Query(..., description="入场价格"),
    stop_loss_price: float = Query(..., description="止损价格"),
    direction: str = Query(..., description="交易方向: BUY 或 SELL"),
    notional_usdt: float = Query(..., description="下单名义价值（USDT）"),
    leverage: int = Query(10, description="杠杆倍数"),
    account_id: int = Depends(get_account_id)
):
    """根据推荐信息手动开仓"""
    try:
        logger.info("=" * 60)
        logger.info(f"收到手动开仓请求: {symbol}")
        logger.info(f"  入场价: {entry_price}, 止损价: {stop_loss_price}")
        logger.info(f"  方向: {direction}, 名义价值: {notional_usdt} USDT, 杠杆: {leverage}x")
        logger.info("=" * 60)
        
        if direction not in ('BUY', 'SELL'):
            raise HTTPException(status_code=400, detail="交易方向必须是 BUY 或 SELL")
        
        if notional_usdt <= 0:
            raise HTTPException(status_code=400, detail="下单名义价值必须大于0")
        
        if entry_price <= 0 or stop_loss_price <= 0:
            raise HTTPException(status_code=400, detail="入场价和止损价必须大于0")
        
        api_key, api_secret, use_testnet, status = Account.get_credentials(account_id)
        
        if (not api_key or not api_secret) and status == "active":
            error_msg = f"API密钥未配置（account_id={account_id}）"
            logger.warning(f"[account_id={account_id}] {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
        
        # 创建客户端
        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:
            # 设置杠杆
            await client.set_leverage(symbol, leverage)
            logger.info(f"✓ 已设置杠杆: {leverage}x")
            
            # 获取交易对信息
            symbol_info = await client.get_symbol_info(symbol)
            if not symbol_info:
                raise HTTPException(status_code=400, detail=f"无法获取 {symbol} 的交易对信息")
            
            # 计算下单数量：数量 = 名义价值 / 入场价
            quantity = notional_usdt / entry_price
            logger.info(f"计算下单数量: {quantity:.8f} (名义价值: {notional_usdt} USDT / 入场价: {entry_price})")
            
            # 调整数量精度
            adjusted_quantity = client._adjust_quantity_precision(quantity, symbol_info)
            if adjusted_quantity <= 0:
                raise HTTPException(status_code=400, detail=f"调整后的数量无效: {adjusted_quantity}")
            
            logger.info(f"调整后的数量: {adjusted_quantity:.8f}")
            
            # 检查最小名义价值
            min_notional = symbol_info.get('minNotional', 5.0)
            actual_notional = adjusted_quantity * entry_price
            if actual_notional < min_notional:
                raise HTTPException(
                    status_code=400,
                    detail=f"订单名义价值不足: {actual_notional:.2f} USDT < 最小要求: {min_notional:.2f} USDT"
                )
            
            # 下 limit 订单
            logger.info(f"开始下 limit 订单: {symbol} {direction} {adjusted_quantity} @ {entry_price}")
            order = await client.place_order(
                symbol=symbol,
                side=direction,
                quantity=adjusted_quantity,
                order_type='LIMIT',
                price=entry_price,
                reduce_only=False
            )
            
            if not order:
                raise HTTPException(status_code=500, detail="下单失败：币安API返回None")
            
            order_id = order.get('orderId')
            logger.info(f"✓ 订单已提交: orderId={order_id}")
            
            # 等待订单成交（最多等待30秒）
            import asyncio
            filled_order = None
            for i in range(30):
                await asyncio.sleep(1)
                try:
                    order_status = await client.client.futures_get_order(symbol=symbol, orderId=order_id)
                    if order_status.get('status') == 'FILLED':
                        filled_order = order_status
                        logger.info(f"✓ 订单已成交: orderId={order_id}")
                        break
                    elif order_status.get('status') in ('CANCELED', 'EXPIRED', 'REJECTED'):
                        raise HTTPException(status_code=400, detail=f"订单未成交，状态: {order_status.get('status')}")
                except Exception as e:
                    if i == 29:  # 最后一次尝试
                        logger.warning(f"订单状态查询失败或未成交: {e}")
                    continue
            
            if not filled_order:
                logger.warning(f"订单 {order_id} 在30秒内未成交，但订单已提交")
                return {
                    "message": f"{symbol} 订单已提交但未成交（请稍后检查）",
                    "symbol": symbol,
                    "order_id": order_id,
                    "status": "pending"
                }
            
            # 订单已成交，保存到数据库
            avg_price = float(filled_order.get('avgPrice', entry_price))
            executed_qty = float(filled_order.get('executedQty', adjusted_quantity))
            
            # 计算实际使用的名义价值和保证金
            actual_notional = executed_qty * avg_price
            actual_margin = actual_notional / leverage
            
            # 保存交易记录
            # trade_id = Trade.create(
            #     account_id=account_id,
            #     symbol=symbol,
            #     side=direction,
            #     quantity=executed_qty,
            #     entry_price=avg_price,
            #     leverage=leverage,
            #     entry_order_id=order_id,
            #     entry_reason='manual_from_recommendation',
            #     notional_usdt=actual_notional,
            #     margin_usdt=actual_margin,
            #     stop_loss_price=stop_loss_price,
            #     # 如果有推荐中的止盈价，也可以传入，这里先不传
            # )
            
            # logger.info(f"✓ 交易记录已保存: trade_id={trade_id}")
            
            # 尝试挂止损/止盈保护单（如果系统支持）
            # try:
            #     # 这里可以调用 _ensure_exchange_sltp_for_symbol 来挂保护单
            #     # 但需要先获取持仓信息来确定方向
            #     positions = await client.get_open_positions()
            #     position = next((p for p in positions if p['symbol'] == symbol), None)
            #     if position:
            #         # 可以在这里挂止损单，但需要知道 take_profit_price
            #         # 暂时只记录止损价到数据库，由系统自动监控
            #         logger.info(f"止损价已记录到数据库: {stop_loss_price}")
            # except Exception as e:
            #     logger.warning(f"挂保护单失败（不影响开仓）: {e}")
            
            return {
                "message": f"{symbol} 开仓成功",
                "symbol": symbol,
                "order_id": order_id,
                "trade_id": None,
                "quantity": executed_qty,
                "entry_price": avg_price,
                "notional_usdt": actual_notional,
                "margin_usdt": actual_margin,
                "status": "filled"
            }
            
        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("/spot/order")
async def place_spot_order(
    symbol: str = Query(..., description="交易对，如 BTCUSDT"),
    side: str = Query("BUY", description="BUY 或 SELL"),
    quote_order_qty: float = Query(..., description="下单金额（USDT），市价单时使用"),
    order_type: str = Query("MARKET", description="MARKET 或 LIMIT"),
    price: float = Query(None, description="限价单价格（LIMIT 时必填）"),
    account_id: int = Depends(get_account_id),
):
    """
    现货一键下单。参考文档：POST /api/v3/order（现货）。
    市价单使用 quoteOrderQty 表示要花费/收到的 USDT 数量；限价单使用 quantity+price。
    """
    try:
        if side not in ("BUY", "SELL"):
            raise HTTPException(status_code=400, detail="side 必须是 BUY 或 SELL")
        if quote_order_qty <= 0:
            raise HTTPException(status_code=400, detail="下单金额必须大于 0")
        # 币安现货多数交易对 MIN_NOTIONAL 为 5 USDT
        if quote_order_qty < 5:
            raise HTTPException(
                status_code=400,
                detail="现货下单金额不能低于 5 USDT（满足 MIN_NOTIONAL），请调大默认下单金额",
            )
        if order_type.upper() == "LIMIT" and (price is None or price <= 0):
            raise HTTPException(status_code=400, detail="限价单必须填写 price")

        api_key, api_secret, use_testnet, status = Account.get_credentials(account_id)
        if (not api_key or not api_secret) and status == "active":
            raise HTTPException(status_code=400, detail=f"API密钥未配置（account_id={account_id}）")

        try:
            from binance_client import BinanceClient
            from binance.exceptions import BinanceAPIException
        except ImportError:
            trading_system_path = project_root / "trading_system"
            sys.path.insert(0, str(trading_system_path))
            from binance_client import BinanceClient
            from binance.exceptions import BinanceAPIException

        client = BinanceClient(api_key=api_key, api_secret=api_secret, testnet=use_testnet)
        await client.connect()
        try:
            # 现货接口：POST /api/v3/order（与合约 fapi 不同）。python-binance 中 create_order 为现货。
            # 文档：市价单可用 quoteOrderQty 设置 quote asset 数量，传字符串避免精度问题。
            quote_str = f"{quote_order_qty:.2f}"

            if order_type.upper() == "MARKET":
                order = await client.client.create_order(
                    symbol=symbol.strip().upper(),
                    side=side,
                    type="MARKET",
                    quoteOrderQty=quote_str,
                )
            else:
                qty = quote_order_qty / price
                # 限价单需要 quantity + price，quantity 需符合交易对 LOT_SIZE
                order = await client.client.create_order(
                    symbol=symbol.strip().upper(),
                    side=side,
                    type="LIMIT",
                    timeInForce="GTC",
                    quantity=round(qty, 8),
                    price=price,
                )
            return {
                "message": f"现货订单已提交: {symbol} {side}",
                "symbol": symbol,
                "order_id": order.get("orderId"),
                "client_order_id": order.get("clientOrderId"),
                "status": order.get("status"),
            }
        finally:
            await client.disconnect()
    except HTTPException:
        raise
    except BinanceAPIException as e:
        code = getattr(e, "code", getattr(e, "status_code", ""))
        msg = str(e).strip() or getattr(e, "message", "")
        if code == -2015:
            detail = "API Key 无现货交易权限或无效。请在币安 API 管理中勾选「启用现货与杠杆交易」并使用该 Key。"
        elif code == -1013:
            detail = f"订单不满足交易对规则（如 MIN_NOTIONAL 约 5 USDT）: {msg}"
        elif code == -1121:
            detail = f"交易对格式无效: {msg}"
        else:
            detail = f"币安现货接口错误 (code={code}): {msg}"
        logger.warning("现货下单 BinanceAPIException: %s", detail)
        raise HTTPException(status_code=400, detail=detail)
    except Exception as e:
        logger.exception("现货下单失败: %s", e)
        raise HTTPException(status_code=500, detail=f"现货下单失败: {str(e)}")


def _order_is_sltp(o: dict, type_key: str = "type") -> bool:
    """判断是否为止损/止盈类订单（含普通单与 Algo 条件单）"""
    t = str(o.get(type_key) or o.get("orderType") or "").upper()
    return t in ("STOP_MARKET", "TAKE_PROFIT_MARKET", "STOP", "TAKE_PROFIT")


@router.post("/positions/sync")
async def sync_positions(
    account_id: int = Depends(get_account_id),
    only_recover_when_has_sltp: bool = Query(True, description="仅当该持仓存在止损/止盈单时才补建记录（用于区分系统单，减少手动单误建）"),
):
    """同步币安实际持仓状态与数据库状态"""
    try:
        logger.info("=" * 60)
        logger.info("收到持仓状态同步请求")
        logger.info("=" * 60)
        
        api_key, api_secret, use_testnet, status = Account.get_credentials(account_id)
        
        if (not api_key or not api_secret) and status == "active":
            error_msg = f"API密钥未配置（account_id={account_id}）"
            logger.warning(f"[account_id={account_id}] {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', account_id=account_id, limit=500)
            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:
                        # 尝试从币安历史订单获取“真实平仓信息”（价格/时间/原因/订单号）
                        latest_close_order = None
                        try:
                            end_time_ms = int(time.time() * 1000)
                            start_time_ms = end_time_ms - (7 * 24 * 60 * 60 * 1000)
                            orders = await client.client.futures_get_all_orders(
                                symbol=symbol,
                                startTime=start_time_ms,
                                endTime=end_time_ms,
                            )
                            if isinstance(orders, list) and orders:
                                close_orders = [
                                    o for o in orders
                                    if isinstance(o, dict)
                                    and o.get("reduceOnly") is True
                                    and o.get("status") == "FILLED"
                                ]
                                if close_orders:
                                    close_orders.sort(key=lambda x: x.get("updateTime", 0), reverse=True)
                                    latest_close_order = close_orders[0]
                        except Exception:
                            latest_close_order = None

                        # 获取该交易对的 open 记录（仅当前账号，避免误更新其他账号）
                        open_trades = Trade.get_by_symbol(symbol, status='open', account_id=account_id)
                        
                        for trade in open_trades:
                            trade_id = trade['id']
                            entry_price = float(trade['entry_price'])
                            quantity = float(trade['quantity'])
                            
                            # 获取当前价格作为平仓价格
                            exit_price = None
                            exit_order_id = None
                            exit_time_ts = None
                            exit_reason = "sync"
                            otype = ""

                            if latest_close_order and isinstance(latest_close_order, dict):
                                try:
                                    exit_price = float(latest_close_order.get("avgPrice", 0) or 0) or None
                                except Exception:
                                    exit_price = None
                                exit_order_id = latest_close_order.get("orderId") or None
                                otype = str(
                                    latest_close_order.get("type")
                                    or latest_close_order.get("origType")
                                    or ""
                                ).upper()
                                try:
                                    ms = latest_close_order.get("updateTime") or latest_close_order.get("time")
                                    if ms:
                                        exit_time_ts = int(int(ms) / 1000)
                                except Exception:
                                    exit_time_ts = None

                                # 检查订单的 reduceOnly 字段：如果是 true，说明是自动平仓，不应该标记为 manual
                                is_reduce_only = latest_close_order.get("reduceOnly", False) if latest_close_order else False
                                
                                if "TRAILING" in otype:
                                    exit_reason = "trailing_stop"
                                elif "TAKE_PROFIT" in otype:
                                    exit_reason = "take_profit"
                                elif "STOP" in otype:
                                    exit_reason = "stop_loss"
                                elif otype in ("MARKET", "LIMIT"):
                                    # 如果是 reduceOnly 订单，说明是自动平仓（可能是保护单触发的），先标记为 sync，后续用价格判断
                                    if is_reduce_only:
                                        exit_reason = "sync"  # 临时标记，后续用价格判断
                                    else:
                                        exit_reason = "manual"  # 非 reduceOnly 的 MARKET/LIMIT 订单才是真正的手动平仓

                            # 价格兜底：如果能明显命中止损/止盈价，则覆盖 exit_reason
                            # 这对于保护单触发的 MARKET 订单特别重要
                            if exit_reason == "sync" or exit_reason == "manual":
                                try:
                                    def _close_to(a: float, b: float, max_pct: float = 0.02) -> bool:
                                        if a <= 0 or b <= 0:
                                            return False
                                        return abs((a - b) / b) <= max_pct

                                    ep = float(exit_price or 0)
                                    if ep > 0:
                                        sl = trade.get("stop_loss_price")
                                        tp = trade.get("take_profit_price")
                                        tp1 = trade.get("take_profit_1")
                                        tp2 = trade.get("take_profit_2")
                                        # 优先检查止损
                                        if sl is not None and _close_to(ep, float(sl), max_pct=0.02):
                                            exit_reason = "stop_loss"
                                        # 然后检查止盈
                                        elif tp is not None and _close_to(ep, float(tp), max_pct=0.02):
                                            exit_reason = "take_profit"
                                        elif tp1 is not None and _close_to(ep, float(tp1), max_pct=0.02):
                                            exit_reason = "take_profit"
                                        elif tp2 is not None and _close_to(ep, float(tp2), max_pct=0.02):
                                            exit_reason = "take_profit"
                                        # 如果价格接近入场价，可能是移动止损触发的
                                        elif is_reduce_only:
                                            entry_price_val = float(trade.get("entry_price", 0) or 0)
                                            if entry_price_val > 0 and _close_to(ep, entry_price_val, max_pct=0.01):
                                                exit_reason = "trailing_stop"
                                except Exception:
                                    pass

                            if not exit_price or exit_price <= 0:
                                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

                            # 从币安成交获取手续费与实际盈亏，保证统计与币安一致
                            sync_commission = None
                            sync_commission_asset = None
                            sync_realized_pnl = None
                            if exit_order_id:
                                try:
                                    recent_trades = await client.get_recent_trades(symbol, limit=30)
                                    related = [t for t in recent_trades if str(t.get('orderId')) == str(exit_order_id)]
                                    if related:
                                        sync_commission = sum(float(t.get('commission', 0)) for t in related)
                                        assets = {t.get('commissionAsset') for t in related if t.get('commissionAsset')}
                                        sync_commission_asset = "/".join(assets) if assets else None
                                        sync_realized_pnl = sum(float(t.get('realizedPnl', 0)) for t in related)
                                except Exception as fee_err:
                                    logger.debug(f"同步 {symbol} 平仓手续费失败: {fee_err}")
                            
                            # 更新数据库记录
                            duration_minutes = None
                            try:
                                et = trade.get("entry_time")
                                if et is not None and exit_time_ts is not None:
                                    et_i = int(et)
                                    if exit_time_ts >= et_i:
                                        duration_minutes = int((exit_time_ts - et_i) / 60)
                            except Exception:
                                duration_minutes = None

                            Trade.update_exit(
                                trade_id=trade_id,
                                exit_price=exit_price,
                                exit_reason=exit_reason,
                                pnl=pnl,
                                pnl_percent=pnl_percent_margin,  # 使用基于保证金的盈亏百分比
                                exit_order_id=exit_order_id,
                                duration_minutes=duration_minutes,
                                exit_time_ts=exit_time_ts,
                                commission=sync_commission,
                                commission_asset=sync_commission_asset or None,
                                realized_pnl=sync_realized_pnl,
                            )
                            updated_count += 1
                            logger.info(
                                f"✓ {symbol} 已更新为closed (ID: {trade_id}, "
                                f"盈亏: {pnl:.2f} USDT, {pnl_percent_margin:.2f}% of margin, "
                                f"原因: {exit_reason}, 类型: {otype or '-'}"
                                f")"
                            )
                    except Exception as e:
                        logger.error(f"❌ {symbol} 更新失败: {e}")
                        import traceback
                        logger.error(f"   错误详情:\n{traceback.format_exc()}")
            else:
                logger.info("✓ 数据库与币安状态一致，无需更新")
            
            # 4. 币安有仓但数据库无记录：优先用「开仓订单 clientOrderId 前缀」判断是否系统单，仅对系统单补建
            missing_in_db = binance_symbols - db_open_symbols
            recovered_count = 0
            system_order_prefix = ""
            try:
                from database.models import TradingConfig, GlobalStrategyConfig
                system_order_prefix = (
                    TradingConfig.get_value("SYSTEM_ORDER_ID_PREFIX", None, account_id=account_id)
                    or GlobalStrategyConfig.get_value("SYSTEM_ORDER_ID_PREFIX", "")
                    or ""
                )
                system_order_prefix = (system_order_prefix or "").strip()
                # 无配置时用默认前缀 ats_，与 position_manager 一致，便于补建「系统限价/条件单事后成交」的记录
                if not system_order_prefix:
                    system_order_prefix = "ats_"
            except Exception:
                system_order_prefix = "ats_"
            if missing_in_db:
                logger.info(f"发现 {len(missing_in_db)} 个持仓在币安存在但数据库中没有记录: {', '.join(missing_in_db)}")
                if system_order_prefix:
                    logger.info(f"  → 对开仓订单 clientOrderId 前缀为「{system_order_prefix}」的持仓补建（系统单）；匹配到则不再要求有止损/止盈单")
                elif only_recover_when_has_sltp:
                    logger.info("  → 仅对「存在止损/止盈单」的持仓补建记录（视为系统单），避免手动单误建")
                for symbol in missing_in_db:
                    try:
                        pos = next((p for p in binance_positions if p.get('symbol') == symbol), None)
                        if not pos or float(pos.get('positionAmt', 0)) == 0:
                            continue
                        position_amt = float(pos['positionAmt'])
                        quantity = abs(position_amt)
                        side = 'BUY' if position_amt > 0 else 'SELL'
                        entry_price = float(pos.get('entryPrice', 0))
                        leverage = int(pos.get('leverage', 10)) or 10
                        notional = quantity * entry_price
                        if notional < 1.0:
                            continue
                        # 补建时尽量拿到 entry_order_id：优先 get_all_orders+clientOrderId 前缀，兜底 get_recent_trades(100)+重试
                        entry_order_id = None
                        client_order_id = None
                        if system_order_prefix:
                            try:
                                end_ms = int(time.time() * 1000)
                                start_ms = end_ms - (24 * 3600 * 1000)
                                orders = await client.client.futures_get_all_orders(
                                    symbol=symbol, startTime=start_ms, endTime=end_ms, recvWindow=20000
                                )
                                if isinstance(orders, list):
                                    open_orders = [
                                        o for o in orders
                                        if isinstance(o, dict) and o.get("reduceOnly") is False
                                        and str(o.get("side", "")).upper() == side and o.get("status") == "FILLED"
                                    ]
                                    our_orders = [o for o in open_orders if (o.get("clientOrderId") or "").startswith(system_order_prefix)]
                                    if our_orders:
                                        our_orders.sort(key=lambda x: int(x.get("updateTime", 0)), reverse=True)
                                        best = None
                                        for o in our_orders:
                                            ap = float(o.get("avgPrice") or 0)
                                            eq = float(o.get("executedQty") or o.get("origQty") or 0)
                                            if ap > 0 and abs(ap - entry_price) / max(entry_price, 1e-9) < 0.01 and abs(eq - quantity) < 1e-6:
                                                best = o
                                                break
                                        if best is None:
                                            best = our_orders[0]
                                        entry_order_id = best.get("orderId")
                                        client_order_id = (best.get("clientOrderId") or "").strip() or None
                            except Exception as e:
                                logger.debug(f"  {symbol} 补建 get_all_orders 取开仓订单号失败: {e}")
                        if entry_order_id is None:
                            try:
                                trades = await client.get_recent_trades(symbol, limit=100)
                                if not trades:
                                    await asyncio.sleep(2)
                                    trades = await client.get_recent_trades(symbol, limit=100)
                                if trades:
                                    same_side = [t for t in trades if str(t.get('side', '')).upper() == side]
                                    same_side.sort(key=lambda x: int(x.get('time', 0)), reverse=True)
                                    if system_order_prefix and same_side:
                                        for t in same_side[:5]:
                                            oid = t.get("orderId")
                                            if not oid:
                                                continue
                                            try:
                                                info = await client.client.futures_get_order(symbol=symbol, orderId=int(oid), recvWindow=20000)
                                                cid = (info or {}).get("clientOrderId") or ""
                                                if cid.startswith(system_order_prefix):
                                                    entry_order_id = oid
                                                    client_order_id = cid.strip() or None
                                                    break
                                            except Exception:
                                                continue
                                    if entry_order_id is None and same_side:
                                        entry_order_id = same_side[0].get("orderId")
                            except Exception as e:
                                logger.debug(f"获取 {symbol} 成交记录失败: {e}")
                        if entry_order_id and client_order_id is None:
                            try:
                                order_info = await client.client.futures_get_order(symbol=symbol, orderId=int(entry_order_id), recvWindow=20000)
                                client_order_id = (order_info or {}).get("clientOrderId") or None
                                if client_order_id:
                                    client_order_id = client_order_id.strip() or None
                            except Exception:
                                pass
                        is_clearly_manual = False
                        if system_order_prefix and entry_order_id and client_order_id and not client_order_id.startswith(system_order_prefix):
                            is_clearly_manual = True
                            logger.debug(f"  {symbol} 开仓订单 clientOrderId={client_order_id!r} 非系统前缀，视为手动单，跳过补建")
                        elif system_order_prefix and entry_order_id and not client_order_id:
                            try:
                                order_info = await client.client.futures_get_order(symbol=symbol, orderId=int(entry_order_id), recvWindow=20000)
                                cid = (order_info or {}).get("clientOrderId") or ""
                                if cid and not cid.startswith(system_order_prefix):
                                    is_clearly_manual = True
                            except Exception:
                                pass
                        if is_clearly_manual:
                            continue
                        # 已通过系统前缀匹配到开仓订单的，直接补建，不要求有止损/止盈单（系统单一定能对应 DB）
                        is_system_order_by_prefix = bool(
                            system_order_prefix and client_order_id
                            and str(client_order_id).strip().startswith(system_order_prefix)
                        )
                        if only_recover_when_has_sltp and not is_system_order_by_prefix:
                            has_sltp = False
                            try:
                                normal = await client.get_open_orders(symbol)
                                for o in (normal or []):
                                    if _order_is_sltp(o, "type"):
                                        has_sltp = True
                                        break
                                if not has_sltp:
                                    algo = await client.futures_get_open_algo_orders(symbol=symbol, algo_type="CONDITIONAL")
                                    for o in (algo or []):
                                        if _order_is_sltp(o, "orderType"):
                                            has_sltp = True
                                            break
                            except Exception as e:
                                logger.debug(f"检查 {symbol} 止盈止损单失败: {e}")
                            if not has_sltp:
                                logger.debug(f"  {symbol} 无止损/止盈单且未匹配到系统前缀，跳过补建")
                                continue
                        if entry_order_id and hasattr(Trade, 'get_by_entry_order_id'):
                            try:
                                existing = Trade.get_by_entry_order_id(entry_order_id)
                                if existing:
                                    continue
                            except Exception:
                                pass
                        trade_id = Trade.create(
                            symbol=symbol,
                            side=side,
                            quantity=quantity,
                            entry_price=entry_price,
                            leverage=leverage,
                            entry_reason='sync_recovered',
                            entry_order_id=entry_order_id,
                            client_order_id=client_order_id,
                            notional_usdt=notional,
                            margin_usdt=(notional / leverage) if leverage > 0 else None,
                            account_id=account_id,
                        )
                        recovered_count += 1
                        logger.info(f"  ✓ {symbol} 已补建交易记录 (ID: {trade_id}, orderId: {entry_order_id or '-'})")
                    except Exception as e:
                        logger.warning(f"  ✗ {symbol} 补建记录失败: {e}")
                if recovered_count > 0:
                    logger.info(f"共补建 {recovered_count} 条交易记录，将在订单记录与统计中展示")
            
            result = {
                "message": "持仓状态同步完成",
                "binance_positions": len(binance_symbols),
                "db_open_positions": len(db_open_symbols),
                "updated_to_closed": updated_count,
                "recovered_count": recovered_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)
