"""对冲计划:情景测算与全仓建议仓(纯函数,无 IO)."""
from __future__ import annotations

from typing import Any, Optional


def _f(v: Any) -> Optional[float]:
    if v is None or v == "":
        return None
    try:
        return float(v)
    except (TypeError, ValueError):
        return None


def perp_coin_amount(*, contracts: float, contract_size: float) -> float:
    return float(contracts) * float(contract_size or 1.0)


def perp_pnl(
    *,
    direction: str,
    entry: float,
    exit_px: float,
    contracts: float,
    contract_size: float,
) -> float:
    coins = perp_coin_amount(contracts=contracts, contract_size=contract_size)
    d = (direction or "long").strip().lower()
    if d == "short":
        return (float(entry) - float(exit_px)) * coins
    return (float(exit_px) - float(entry)) * coins


def option_premium_total(*, ask: float, sheets: float, ct_mult: float) -> float:
    """卖一报价为每 1 币;权利金 = ask × 张数 × ct_mult."""
    return float(ask) * float(sheets) * float(ct_mult or 0.01)


def option_expiry_pnl(
    *,
    opt_type: str,
    strike: float,
    spot: float,
    sheets: float,
    ct_mult: float,
    premium_paid: float,
) -> float:
    o = (opt_type or "").strip().upper()
    intrinsic_per_coin = 0.0
    if o in ("C", "CALL"):
        intrinsic_per_coin = max(0.0, float(spot) - float(strike))
    elif o in ("P", "PUT"):
        intrinsic_per_coin = max(0.0, float(strike) - float(spot))
    else:
        return -float(premium_paid)
    value = intrinsic_per_coin * float(sheets) * float(ct_mult or 0.01)
    return value - float(premium_paid)


def spot_from_expiry_intrinsic_profit(
    *,
    opt_type: str,
    strike: float,
    sheets: float,
    ct_mult: float,
    premium_paid: float,
    profit: float,
) -> float | None:
    """按到期实值反推现货价:使该腿到期盈亏 ≈ profit.

    到期价值=实值×张数×乘数;盈亏=价值−权利金 → 实值/币=(profit+权利金)/(张数×乘数).
    Call: spot=K+实值/币; Put: spot=K−实值/币.
    """
    try:
        k = float(strike)
        n = float(sheets or 0)
        ct = float(ct_mult or 0.01)
        prem = float(premium_paid or 0)
        pnl = float(profit)
    except (TypeError, ValueError):
        return None
    denom = n * ct
    if denom <= 0:
        return None
    need = (pnl + prem) / denom
    if need < 0:
        need = 0.0
    o = (opt_type or "").strip().upper()
    if o in ("C", "CALL"):
        return round(k + need, 2)
    if o in ("P", "PUT"):
        return round(k - need, 2)
    return None


def suggest_contracts_from_notional(
    *,
    notional: float,
    entry: float,
    contract_size: float,
) -> float:
    if entry <= 0 or contract_size <= 0 or notional <= 0:
        return 0.0
    return float(notional) / (float(entry) * float(contract_size))


def floor_contracts_to_precision(contracts: float, decimals: int) -> float:
    """按交易所张数精度向下取整,避免建议张数超过可用保证金."""
    import math

    raw = float(contracts or 0.0)
    if raw <= 0:
        return 0.0
    try:
        d = int(decimals)
    except (TypeError, ValueError):
        d = 0
    if d <= 0:
        return float(math.floor(raw + 1e-12))
    scale = 10**d
    return math.floor(raw * scale + 1e-12) / scale


def option_unit_cost_usdc(*, ask: float, ct_mult: float) -> float:
    """单张权利金(USDC) = 卖一价 × ct_mult."""
    a = _f(ask)
    if a is None or a <= 0:
        return 0.0
    return float(a) * float(ct_mult or 0.01)


def resolve_oo_budget_usdc(
    *,
    trading_usdc: Any,
    trade_budget_usdc: Any,
    buffer_ratio: Any = 0.95,
) -> dict[str, Any]:
    """期期可用预算 = min(交易户×buffer, 单笔预算)."""
    import math

    trading = _f(trading_usdc)
    cap = _f(trade_budget_usdc)
    buf = _f(buffer_ratio)
    if buf is None or buf <= 0:
        buf = 0.95
    if buf > 1:
        buf = 1.0
    trading_cap = None if trading is None else max(0.0, float(trading) * float(buf))
    trade_cap = None if cap is None else max(0.0, float(cap))
    if trading_cap is None and trade_cap is None:
        return {
            "ok": False,
            "budget_usdc": 0.0,
            "trading_cap": None,
            "trade_budget_cap": None,
            "buffer_ratio": float(buf),
            "msg": "缺少交易户余额与单笔预算",
        }
    if trading_cap is None:
        budget = float(trade_cap or 0.0)
    elif trade_cap is None:
        budget = float(trading_cap)
    else:
        budget = min(float(trading_cap), float(trade_cap))
    budget = float(math.floor(budget * 1e6 + 1e-12) / 1e6)
    return {
        "ok": budget > 0,
        "budget_usdc": budget,
        "trading_cap": None if trading_cap is None else round(float(trading_cap), 6),
        "trade_budget_cap": None if trade_cap is None else round(float(trade_cap), 6),
        "buffer_ratio": float(buf),
        "msg": "" if budget > 0 else "可用预算为 0",
    }


def _cap_sheets_by_ask_depth(sheets: int, ask_sz: Any) -> int:
    import math

    n = max(0, int(sheets))
    depth = _f(ask_sz)
    if depth is None:
        return n
    if depth <= 0:
        return 0
    return min(n, int(math.floor(float(depth) + 1e-12)))


def _normalize_oo_sheets_mode(mode: str) -> str:
    m = (mode or "same_sheets").strip().lower()
    if m in ("long_bias", "bias_long", "long", "做多"):
        return "long_bias"
    if m in ("short_bias", "bias_short", "short", "做空"):
        return "short_bias"
    # 旧「均分」兼容:按预算 50/50(页面已移除)
    if m in ("split", "equal_budget", "split_budget", "均分"):
        return "split_budget"
    return "same_sheets"


def _normalize_oo_bias_split_by(raw: Any) -> str:
    v = str(raw or "budget").strip().lower()
    if v in ("sheets", "qty", "quantity", "张数"):
        return "sheets"
    return "budget"


def _clamp_oo_bias_ratio(raw: Any, default: float = 0.7) -> float:
    try:
        r = float(raw)
    except (TypeError, ValueError):
        r = float(default)
    if r <= 0 or r >= 1:
        r = float(default)
    return r


def _oo_call_put_leg_index(opt_type_a: str, opt_type_b: str) -> tuple[Optional[str], Optional[str], str]:
    """返回 (call_side, put_side, err);side 为 'a'/'b'."""
    a = (opt_type_a or "").strip().upper()
    b = (opt_type_b or "").strip().upper()
    if a.startswith("C"):
        a = "C"
    elif a.startswith("P"):
        a = "P"
    if b.startswith("C"):
        b = "C"
    elif b.startswith("P"):
        b = "P"
    if {a, b} != {"C", "P"}:
        return None, None, "做多/做空需一腿 Call、一腿 Put"
    call_side = "a" if a == "C" else "b"
    put_side = "b" if call_side == "a" else "a"
    return call_side, put_side, ""


def suggest_oo_sheets(
    *,
    mode: str,
    budget_usdc: float,
    ask_a: float,
    ct_mult_a: float = 0.01,
    ask_sz_a: Any = None,
    opt_type_a: str = "",
    ask_b: float,
    ct_mult_b: float = 0.01,
    ask_sz_b: Any = None,
    opt_type_b: str = "",
    bias_split_by: str = "budget",
    bias_ratio: float = 0.7,
) -> dict[str, Any]:
    """期期建议张数:same_sheets / long_bias / short_bias(及旧 split_budget)."""
    import math

    m = _normalize_oo_sheets_mode(mode)
    split_by = _normalize_oo_bias_split_by(bias_split_by)
    ratio = _clamp_oo_bias_ratio(bias_ratio)
    budget = max(0.0, float(budget_usdc or 0.0))
    cost_a = option_unit_cost_usdc(ask=ask_a, ct_mult=ct_mult_a)
    cost_b = option_unit_cost_usdc(ask=ask_b, ct_mult=ct_mult_b)

    def _fail(msg: str, n_a: int = 0, n_b: int = 0) -> dict[str, Any]:
        return {
            "mode": m,
            "sheets_a": n_a,
            "sheets_b": n_b,
            "cost_a": round(cost_a, 8),
            "cost_b": round(cost_b, 8),
            "premium_est": round(cost_a * n_a + cost_b * n_b, 6),
            "ok": False,
            "msg": msg,
            "bias_split_by": split_by,
            "bias_ratio": ratio,
        }

    if budget <= 0:
        return _fail("可用预算为 0")
    if cost_a <= 0 or cost_b <= 0:
        return _fail("缺少有效卖一价,无法建议张数")

    pair = cost_a + cost_b
    n_pair = int(math.floor(budget / pair + 1e-12)) if pair > 0 else 0
    # 与同张数一致:先按预算得 n,再各自深度封顶后取 min
    n_same = min(
        _cap_sheets_by_ask_depth(n_pair, ask_sz_a),
        _cap_sheets_by_ask_depth(n_pair, ask_sz_b),
    )

    if m == "same_sheets":
        n_a = n_same
        n_b = n_same
    elif m == "split_budget":
        half = budget / 2.0
        n_a = int(math.floor(half / cost_a + 1e-12))
        n_b = int(math.floor(half / cost_b + 1e-12))
        n_a = _cap_sheets_by_ask_depth(n_a, ask_sz_a)
        n_b = _cap_sheets_by_ask_depth(n_b, ask_sz_b)
    else:
        call_side, put_side, err = _oo_call_put_leg_index(opt_type_a, opt_type_b)
        if err:
            return _fail(err)
        major_is_call = m == "long_bias"
        if split_by == "sheets":
            # 总张数 = 同张数两侧合计(每腿 n → 共 2n),再按比例拆到 Call/Put
            total = int(n_same) * 2
            if total < 2:
                return _fail("同张数总规模不足 2,无法按比例拆分")
            major_n = int(round(total * ratio))
            major_n = max(1, min(major_n, total - 1))
            minor_n = total - major_n
            n_call = major_n if major_is_call else minor_n
            n_put = minor_n if major_is_call else major_n
        else:
            maj_budget = budget * ratio
            min_budget = budget * (1.0 - ratio)
            cost_call = cost_a if call_side == "a" else cost_b
            cost_put = cost_b if call_side == "a" else cost_a
            if major_is_call:
                n_call = int(math.floor(maj_budget / cost_call + 1e-12)) if cost_call > 0 else 0
                n_put = int(math.floor(min_budget / cost_put + 1e-12)) if cost_put > 0 else 0
            else:
                n_put = int(math.floor(maj_budget / cost_put + 1e-12)) if cost_put > 0 else 0
                n_call = int(math.floor(min_budget / cost_call + 1e-12)) if cost_call > 0 else 0
        n_a = n_call if call_side == "a" else n_put
        n_b = n_put if call_side == "a" else n_call
        n_a = _cap_sheets_by_ask_depth(n_a, ask_sz_a)
        n_b = _cap_sheets_by_ask_depth(n_b, ask_sz_b)

    prem = cost_a * n_a + cost_b * n_b
    ok = n_a >= 1 and n_b >= 1
    msg = "" if ok else "预算不够开 1+1(或卖一深度不足)"
    return {
        "mode": m,
        "sheets_a": n_a,
        "sheets_b": n_b,
        "cost_a": round(cost_a, 8),
        "cost_b": round(cost_b, 8),
        "premium_est": round(prem, 6),
        "ok": ok,
        "msg": msg,
        "bias_split_by": split_by,
        "bias_ratio": ratio,
    }


def build_perp_options_preview(
    *,
    direction: str,
    entry: float,
    tp: float,
    sl: float,
    contracts: float,
    contract_size: float,
    opt_type: str,
    strike: float,
    sheets: float,
    ct_mult: float,
    premium_paid: float,
    index_px: Optional[float] = None,
) -> dict[str, Any]:
    """
    永期情景.
    止盈账:永续止盈盈利 - 权利金.
    止损账:期权到期内在(按 SL 价) - 永续止损亏损额.
    """
    d = (direction or "long").strip().lower()
    pnl_tp_perp = perp_pnl(
        direction=d, entry=entry, exit_px=tp, contracts=contracts, contract_size=contract_size
    )
    pnl_sl_perp = perp_pnl(
        direction=d, entry=entry, exit_px=sl, contracts=contracts, contract_size=contract_size
    )
    # 止盈统计口径
    tp_total = float(pnl_tp_perp) - float(premium_paid)
    # 止损:期权按 SL 价结算内在 - |永续亏损|
    opt_at_sl = option_expiry_pnl(
        opt_type=opt_type,
        strike=strike,
        spot=sl,
        sheets=sheets,
        ct_mult=ct_mult,
        premium_paid=premium_paid,
    )
    sl_total = float(opt_at_sl) - abs(float(pnl_sl_perp)) if pnl_sl_perp < 0 else float(opt_at_sl) + float(
        pnl_sl_perp
    )
    # 有符号相加更稳:期权盈亏 + 永续盈亏
    sl_total_signed = float(opt_at_sl) + float(pnl_sl_perp)

    spot = float(index_px) if index_px is not None else float(entry)
    opt_flat = option_expiry_pnl(
        opt_type=opt_type,
        strike=strike,
        spot=spot,
        sheets=sheets,
        ct_mult=ct_mult,
        premium_paid=premium_paid,
    )
    flat_total = 0.0 + float(opt_flat)

    opt_at_tp = option_expiry_pnl(
        opt_type=opt_type,
        strike=strike,
        spot=tp,
        sheets=sheets,
        ct_mult=ct_mult,
        premium_paid=premium_paid,
    )

    return {
        "plan_type": "perp_options",
        "direction": d,
        "contracts": contracts,
        "coin_amount": perp_coin_amount(contracts=contracts, contract_size=contract_size),
        "premium_paid": round(float(premium_paid), 6),
        "scenarios": [
            {
                "id": "tp",
                "label": "止盈(计划结束口径)",
                "spot": tp,
                "perp_pnl": round(pnl_tp_perp, 4),
                "options_pnl": round(-float(premium_paid), 4),
                "total": round(tp_total, 4),
                "note": "止盈盈利 − 权利金;期权可不强平",
            },
            {
                "id": "sl",
                "label": "止损(计划结束口径)",
                "spot": sl,
                "perp_pnl": round(pnl_sl_perp, 4),
                "options_pnl": round(opt_at_sl, 4),
                "total": round(sl_total_signed, 4),
                "note": "期权盈利 − 永续亏损(有符号相加);期权须强平",
            },
            {
                "id": "flat",
                "label": "到期·现价附近",
                "spot": spot,
                "perp_pnl": 0.0,
                "options_pnl": round(opt_flat, 4),
                "total": round(flat_total, 4),
                "note": "示意:永续未动,期权按到期内在",
            },
            {
                "id": "expiry_tp",
                "label": "到期·止盈价",
                "spot": tp,
                "perp_pnl": round(pnl_tp_perp, 4),
                "options_pnl": round(opt_at_tp, 4),
                "total": round(pnl_tp_perp + opt_at_tp, 4),
                "note": "若期权拿到 TP 价到期(参考)",
            },
            {
                "id": "expiry_sl",
                "label": "到期·止损价",
                "spot": sl,
                "perp_pnl": round(pnl_sl_perp, 4),
                "options_pnl": round(opt_at_sl, 4),
                "total": round(pnl_sl_perp + opt_at_sl, 4),
                "note": "与止损口径相近(期权用内在)",
            },
        ],
        "summary": {
            "tp_total": round(tp_total, 4),
            "sl_total": round(sl_total_signed, 4),
            "premium_paid": round(float(premium_paid), 4),
            "hedge_ratio_at_sl": _hedge_ratio(opt_at_sl, pnl_sl_perp),
        },
    }


def _hedge_ratio(opt_pnl: float, perp_pnl: float) -> Optional[float]:
    loss = abs(float(perp_pnl)) if float(perp_pnl) < 0 else 0.0
    if loss <= 1e-12:
        return None
    if float(opt_pnl) <= 0:
        return 0.0
    return round(float(opt_pnl) / loss * 100.0, 2)


def build_options_options_preview(
    *,
    target_price: float | None = None,
    target_price_up: float | None = None,
    target_price_down: float | None = None,
    profit_rr: float | None = None,
    index_px: float,
    leg_a: dict[str, Any],
    leg_b: dict[str, Any],
) -> dict[str, Any]:
    """期期情景:盈亏比达标 / 到期现价 / 最大保费损耗.

    新口径优先 profit_rr(盈利金额/总权利金);若未传则兼容旧上/下破目标价.
    残值按亏损腿本合约权利金的 20% 计.
    """

    def _leg_pnl(leg: dict[str, Any], spot: float) -> float:
        return option_expiry_pnl(
            opt_type=str(leg.get("opt_type") or ""),
            strike=float(leg["strike"]),
            spot=spot,
            sheets=float(leg.get("sheets") or 0),
            ct_mult=float(leg.get("ct_mult") or 0.01),
            premium_paid=float(leg.get("premium_paid") or 0),
        )

    prem_a = float(leg_a.get("premium_paid") or 0)
    prem_b = float(leg_b.get("premium_paid") or 0)
    prem = prem_a + prem_b
    rr = float(profit_rr) if profit_rr is not None else None

    # 新:盈亏比情景(不依赖指数上下破价)
    if rr is not None and rr > 0:
        # 盈利腿达 RR:盈利金额 = rr × 总权利金;亏损腿按全亏 / 本合约残值20%回收
        win_profit = rr * prem
        a_at_a = win_profit
        b_at_a_full = -prem_b
        b_at_a_res = -prem_b * 0.8  # 本合约回收 20%
        b_at_b = win_profit
        a_at_b_full = -prem_a
        a_at_b_res = -prem_a * 0.8

        spot_a = spot_from_expiry_intrinsic_profit(
            opt_type=str(leg_a.get("opt_type") or ""),
            strike=float(leg_a["strike"]),
            sheets=float(leg_a.get("sheets") or 0),
            ct_mult=float(leg_a.get("ct_mult") or 0.01),
            premium_paid=prem_a,
            profit=win_profit,
        )
        spot_b = spot_from_expiry_intrinsic_profit(
            opt_type=str(leg_b.get("opt_type") or ""),
            strike=float(leg_b["strike"]),
            sheets=float(leg_b.get("sheets") or 0),
            ct_mult=float(leg_b.get("ct_mult") or 0.01),
            premium_paid=prem_b,
            profit=win_profit,
        )

        a_flat = _leg_pnl(leg_a, index_px)
        b_flat = _leg_pnl(leg_b, index_px)
        flat_total = a_flat + b_flat

        return {
            "plan_type": "options_options",
            "premium_paid": round(prem, 6),
            "profit_rr": rr,
            "target_price": None,
            "target_price_up": None,
            "target_price_down": None,
            "winner_at_up": "a",
            "winner_at_down": "b",
            "winner_at_target": "a",
            "scenarios": [
                {
                    "id": "rr_leg_a_full",
                    "label": f"腿A达盈亏比{rr:g}(亏腿全损)",
                    "spot": spot_a,
                    "leg_a_pnl": round(a_at_a, 4),
                    "leg_b_pnl": round(b_at_a_full, 4),
                    "total": round(a_at_a + b_at_a_full, 4),
                    "note": "现货=到期实值反推;盈利=总权利金×盈亏比;亏腿本合约全亏",
                },
                {
                    "id": "rr_leg_b_full",
                    "label": f"腿B达盈亏比{rr:g}(亏腿全损)",
                    "spot": spot_b,
                    "leg_a_pnl": round(a_at_b_full, 4),
                    "leg_b_pnl": round(b_at_b, 4),
                    "total": round(a_at_b_full + b_at_b, 4),
                    "note": "现货=到期实值反推;盈利=总权利金×盈亏比;亏腿本合约全亏",
                },
                {
                    "id": "rr_leg_a_residual",
                    "label": f"腿A达盈亏比{rr:g}(亏腿残值20%)",
                    "spot": spot_a,
                    "leg_a_pnl": round(a_at_a, 4),
                    "leg_b_pnl": round(b_at_a_res, 4),
                    "total": round(a_at_a + b_at_a_res, 4),
                    "note": "现货同腿A达标反推;亏腿买一回收约本合约权利金20%",
                },
                {
                    "id": "expiry_flat",
                    "label": "到期·现价",
                    "spot": index_px,
                    "leg_a_pnl": round(a_flat, 4),
                    "leg_b_pnl": round(b_flat, 4),
                    "total": round(flat_total, 4),
                    "note": "无盈利则记总亏损结束" if flat_total <= 0 else "到期仍可能有净值",
                },
                {
                    "id": "max_premium_loss",
                    "label": "最大保费损耗",
                    "spot": None,
                    "leg_a_pnl": round(-prem_a, 4),
                    "leg_b_pnl": round(-prem_b, 4),
                    "total": round(-prem, 4),
                    "note": "双腿权利金全部损失",
                },
            ],
            "summary": {
                "profit_rr": rr,
                "spot_at_rr_a": spot_a,
                "spot_at_rr_b": spot_b,
                "at_rr_a_full_total": round(a_at_a + b_at_a_full, 4),
                "at_rr_b_full_total": round(a_at_b_full + b_at_b, 4),
                "at_rr_a_residual_total": round(a_at_a + b_at_a_res, 4),
                "at_target_up_total": round(a_at_a + b_at_a_full, 4),
                "at_target_down_total": round(a_at_b_full + b_at_b, 4),
                "at_target_total": round(a_at_a + b_at_a_full, 4),
                "expiry_flat_total": round(flat_total, 4),
                "premium_paid": round(prem, 6),
                "expiry_is_loss": flat_total <= 0,
                "rr_risk_premium": round(prem, 6),
                "rr_at_up": round((a_at_a + b_at_a_full) / prem, 4) if prem > 0 else None,
                "rr_at_down": round((a_at_b_full + b_at_b) / prem, 4) if prem > 0 else None,
            },
        }

    # 兼容旧单目标:若未传上下目标则用 target_price 填两边
    up = target_price_up if target_price_up is not None else target_price
    down = target_price_down if target_price_down is not None else target_price
    if up is None or down is None:
        raise ValueError("缺少盈亏比或上破/下破目标价")
    up_f = float(up)
    down_f = float(down)

    a_up = _leg_pnl(leg_a, up_f)
    b_up = _leg_pnl(leg_b, up_f)
    at_up = a_up + b_up
    win_up = "a" if a_up >= b_up else "b"

    a_dn = _leg_pnl(leg_a, down_f)
    b_dn = _leg_pnl(leg_b, down_f)
    at_dn = a_dn + b_dn
    win_dn = "a" if a_dn >= b_dn else "b"

    a_flat = _leg_pnl(leg_a, index_px)
    b_flat = _leg_pnl(leg_b, index_px)
    flat_total = a_flat + b_flat
    expiry_loss = flat_total if flat_total <= 0 else flat_total

    return {
        "plan_type": "options_options",
        "premium_paid": round(prem, 6),
        "target_price": up_f,  # 兼容旧字段,取上破
        "target_price_up": up_f,
        "target_price_down": down_f,
        "winner_at_up": win_up,
        "winner_at_down": win_dn,
        "winner_at_target": win_up,
        "scenarios": [
            {
                "id": "target_up",
                "label": "上破目标",
                "spot": up_f,
                "leg_a_pnl": round(a_up, 4),
                "leg_b_pnl": round(b_up, 4),
                "total": round(at_up, 4),
                "note": f"盈利方≈腿{win_up.upper()}(可平);亏损方默认到期",
            },
            {
                "id": "target_down",
                "label": "下破目标",
                "spot": down_f,
                "leg_a_pnl": round(a_dn, 4),
                "leg_b_pnl": round(b_dn, 4),
                "total": round(at_dn, 4),
                "note": f"盈利方≈腿{win_dn.upper()}(可平);亏损方默认到期",
            },
            {
                "id": "expiry_flat",
                "label": "到期·现价(无突破)",
                "spot": index_px,
                "leg_a_pnl": round(a_flat, 4),
                "leg_b_pnl": round(b_flat, 4),
                "total": round(flat_total, 4),
                "note": "无盈利则记总亏损结束" if flat_total <= 0 else "到期仍可能有净值",
            },
            {
                "id": "max_premium_loss",
                "label": "最大保费损耗",
                "spot": None,
                "leg_a_pnl": round(-prem_a, 4),
                "leg_b_pnl": round(-prem_b, 4),
                "total": round(-prem, 4),
                "note": "双腿权利金全部损失",
            },
        ],
        "summary": {
            "at_target_up_total": round(at_up, 4),
            "at_target_down_total": round(at_dn, 4),
            "at_target_total": round(at_up, 4),
            "expiry_flat_total": round(expiry_loss, 4),
            "premium_paid": round(prem, 6),
            "expiry_is_loss": flat_total <= 0,
            # 盈亏比:盈利/全亏保费(风险=权利金全损)
            "rr_risk_premium": round(prem, 6),
            "rr_at_up": round(at_up / prem, 4) if prem > 0 else None,
            "rr_at_down": round(at_dn / prem, 4) if prem > 0 else None,
        },
    }


def gate_status(
    *,
    hedge_enabled: bool,
    sizing_mode: str,
    plan_type: str,
    options_enabled: bool,
    live_order: bool = False,
    live_trading: bool = False,
    active_count: int = 0,
    max_active: int = 1,
    show_perp_options: bool = True,
    show_options_options: bool = True,
    mutual_exclusive: bool = True,
    has_standalone_option: bool = False,
) -> dict[str, Any]:
    from lib.trade.position_sizing_lib import is_full_margin_mode

    full = is_full_margin_mode(sizing_mode)
    pt = (plan_type or "").strip().lower()
    can_preview = True
    can_start = True
    reasons: list[str] = []
    if not hedge_enabled:
        can_start = False
        reasons.append("对冲计划未启用(HEDGE_PLAN_ENABLED)")
    if not options_enabled:
        can_preview = False
        can_start = False
        reasons.append("期权模块未启用")
    if pt == "perp_options" and not show_perp_options:
        can_preview = False
        can_start = False
        reasons.append("永期对冲已隐藏(HEDGE_PLAN_SHOW_PERP_OPTIONS)")
    if pt == "options_options" and not show_options_options:
        can_preview = False
        can_start = False
        reasons.append("期期对冲已隐藏(HEDGE_PLAN_SHOW_OPTIONS_OPTIONS)")
    if not live_order:
        can_start = False
        reasons.append("未允许对冲真实下单(HEDGE_PLAN_LIVE_ORDER)")
    if active_count >= max(1, int(max_active or 1)):
        can_start = False
        reasons.append(f"活跃计划已达上限({max_active})")
    if mutual_exclusive and has_standalone_option:
        can_start = False
        reasons.append("存在单独期权持仓,禁止启动对冲计划(互斥门控)")
    if pt == "perp_options":
        if not full:
            can_start = False
            reasons.append("永期开仓仅全仓模式可用(当前可测算)")
        if not live_trading:
            can_start = False
            reasons.append("未开启实盘(LIVE_TRADING_ENABLED)")
    elif pt == "options_options":
        pass
    else:
        can_start = False
        reasons.append("未知计划类型")
    if can_start:
        reasons = []
    return {
        "hedge_enabled": hedge_enabled,
        "options_enabled": options_enabled,
        "sizing_mode": sizing_mode,
        "is_full_margin": full,
        "plan_type": pt,
        "live_order": live_order,
        "live_trading": live_trading,
        "active_count": active_count,
        "max_active": max_active,
        "show_perp_options": bool(show_perp_options),
        "show_options_options": bool(show_options_options),
        "mutual_exclusive": bool(mutual_exclusive),
        "has_standalone_option": bool(has_standalone_option),
        "can_preview": can_preview,
        "can_start": can_start,
        "reasons": reasons,
    }
