"""OKX 对冲计划:P0 测算页与 API 注册."""
from __future__ import annotations

import os
from typing import Any, Optional

from flask import Flask, jsonify, request
from jinja2 import ChoiceLoader, FileSystemLoader

from lib.hedge_plan.hedge_plan_calc_lib import (
    build_options_options_preview,
    build_perp_options_preview,
    floor_contracts_to_precision,
    gate_status,
    option_premium_total,
    suggest_contracts_from_notional,
)
from lib.hub.hub_calculator_market_lib import amount_decimals_from_exchange
from lib.trade.position_sizing_lib import (
    compute_full_margin_sizing,
    load_position_sizing_mode,
)


def _env_bool(key: str, default: bool = False) -> bool:
    raw = (os.getenv(key) or "").strip().lower()
    if not raw:
        return default
    return raw in ("1", "true", "yes", "on")


def attach_hedge_plan_templates(app: Flask, repo_root: str) -> None:
    tpl_dir = os.path.join(repo_root, "lib", "hedge_plan", "templates")
    if not os.path.isdir(tpl_dir):
        return
    existing = app.jinja_loader
    loaders = [FileSystemLoader(tpl_dir)]
    if existing is not None:
        if isinstance(existing, ChoiceLoader):
            loaders = list(existing.loaders) + loaders
        else:
            loaders.insert(0, existing)
    app.jinja_loader = ChoiceLoader(loaders)


def install_hedge_plan(app: Flask, repo_root: str, app_module: Any) -> None:
    attach_hedge_plan_templates(app, repo_root)
    cfg = _build_cfg(app_module)
    app.extensions["hedge_plan_cfg"] = cfg
    register_hedge_plan_routes(app, cfg)
    _maybe_start_monitor(cfg)


def _build_cfg(app_module: Any) -> dict[str, Any]:
    from lib.exchange.okx_options_lib import (
        build_option_chain,
        fetch_index_price,
        options_header_balances,
        place_option_limit_order,
        quote_option_contract,
        td_mode_for_option_buy,
    )

    def _amount_to_precision(sym: str, amt: float) -> float:
        ex = getattr(app_module, "exchange", None)
        if ex is None:
            return float(amt)
        return float(ex.amount_to_precision(sym, amt))

    return {
        "get_db": app_module.get_db,
        "login_required": app_module.login_required,
        "render_main_page": app_module.render_main_page,
        "exchange": getattr(app_module, "exchange", None),
        "exchange_options": getattr(app_module, "exchange_options", None),
        "get_available_trading_usdt": getattr(app_module, "get_available_trading_usdt", None),
        "get_contract_size": getattr(app_module, "get_contract_size", None),
        "normalize_exchange_symbol": getattr(app_module, "normalize_exchange_symbol", None),
        "ensure_markets_loaded": getattr(app_module, "ensure_markets_loaded", None),
        "ensure_okx_live_ready": getattr(app_module, "ensure_okx_live_ready", None),
        "place_exchange_order": getattr(app_module, "place_exchange_order", None),
        "close_exchange_order": getattr(app_module, "close_exchange_order", None),
        "get_live_position_contracts": getattr(app_module, "get_live_position_contracts", None),
        "amount_to_precision": _amount_to_precision,
        "build_option_chain": build_option_chain,
        "options_header_balances": options_header_balances,
        "quote_option_contract": quote_option_contract,
        "place_option_limit_order": place_option_limit_order,
        "td_mode_for_option_buy": td_mode_for_option_buy,
        "fetch_index_price": fetch_index_price,
        "options_td_mode": (os.getenv("OKX_OPTIONS_TD_MODE") or "isolated").strip(),
        "btc_leverage": int(getattr(app_module, "BTC_LEVERAGE", 10) or 10),
        "alt_leverage": int(getattr(app_module, "ALT_LEVERAGE", 5) or 5),
        "full_margin_buffer": float(getattr(app_module, "FULL_MARGIN_BUFFER_RATIO", 0.98) or 0.98),
        "funds_decimals": int(getattr(app_module, "FUNDS_DECIMALS", 2) or 2),
        "options_enabled": _env_bool("OKX_OPTIONS_ENABLED", False),
        "default_underly": (os.getenv("OKX_OPTIONS_DEFAULT_UNDERLY") or "ETH").strip().upper(),
        "chain_max_dte": float(os.getenv("OKX_OPTIONS_CHAIN_MAX_DTE_DAYS") or os.getenv("OKX_OPTIONS_MAX_DTE_DAYS") or "14"),
        "perp_account_label": (os.getenv("OKX_ACCOUNT_LABEL") or "合约账户").strip(),
        "options_account_label": (os.getenv("OKX_OPTIONS_ACCOUNT_LABEL") or "期权账户").strip(),
        "trade_budget_usdc": float(os.getenv("OKX_OPTIONS_TRADE_BUDGET_USDC") or "10"),
        # 对冲专用缓冲;与期权页 OKX_OPTIONS_BUDGET_BUFFER 独立
        "budget_buffer": float(os.getenv("HEDGE_PLAN_BUDGET_BUFFER") or "0.95"),
        "oo_bias_split_by": _oo_bias_split_by(),
        "oo_bias_ratio": _oo_bias_ratio(),
        "live_trading": _env_bool("LIVE_TRADING_ENABLED", False),
        "send_wechat": getattr(app_module, "send_wechat_msg", None),
    }


def _hedge_enabled() -> bool:
    from lib.hedge_plan.okx_trade_mode_lib import hedge_module_enabled

    return hedge_module_enabled()


def _show_perp_options() -> bool:
    from lib.hedge_plan.okx_trade_mode_lib import show_perp_options

    return show_perp_options()


def _show_options_options() -> bool:
    from lib.hedge_plan.okx_trade_mode_lib import show_options_options

    return show_options_options()


def _oo_close_mode_enabled() -> bool:
    return _env_bool("HEDGE_PLAN_OO_CLOSE_MODE_ENABLED", True)


def _oo_bias_split_by() -> str:
    from lib.hedge_plan.hedge_plan_calc_lib import _normalize_oo_bias_split_by

    return _normalize_oo_bias_split_by(os.getenv("HEDGE_PLAN_OO_BIAS_SPLIT_BY") or "budget")


def _oo_bias_ratio() -> float:
    from lib.hedge_plan.hedge_plan_calc_lib import _clamp_oo_bias_ratio

    return _clamp_oo_bias_ratio(os.getenv("HEDGE_PLAN_OO_BIAS_RATIO") or "0.7")


def _normalize_oo_close_mode(raw: Any) -> str:
    """方案C关闭时强制 hold_expiry;开启时默认 close_all."""
    if not _oo_close_mode_enabled():
        return "hold_expiry"
    v = str(raw or "close_all").strip().lower()
    if v in ("hold_expiry", "hold_to_expiry", "expiry", "到期平"):
        return "hold_expiry"
    return "close_all"


def _live_order() -> bool:
    return _env_bool("HEDGE_PLAN_LIVE_ORDER", False)


def _max_active() -> int:
    try:
        return max(1, int(os.getenv("MAX_ACTIVE_HEDGE_PLANS") or "1"))
    except ValueError:
        return 1


def _gates_dict(cfg: dict[str, Any], plan_type: str) -> dict[str, Any]:
    active = 0
    has_standalone = False
    mutual = True
    try:
        from lib.hedge_plan.hedge_options_exclusive_lib import (
            has_standalone_option_position,
            mutual_exclusive_enabled,
        )
        from lib.hedge_plan.hedge_plan_db import count_active_plans, init_hedge_plan_tables

        mutual = mutual_exclusive_enabled()
        conn = cfg["get_db"]()
        try:
            init_hedge_plan_tables(conn)
            active = count_active_plans(conn)
            if mutual:
                try:
                    from lib.exchange.okx_options_lib import fetch_option_positions

                    ex = cfg.get("exchange_options") or cfg.get("exchange")
                    raw = fetch_option_positions(ex) if ex is not None else []
                    has_standalone = has_standalone_option_position(conn, raw or [])
                except Exception:
                    has_standalone = True  # fail-closed
            conn.commit()
        finally:
            conn.close()
    except Exception:
        # fail-closed:探测失败视为不可开仓
        active = 10**9
        has_standalone = True
    return gate_status(
        hedge_enabled=_hedge_enabled(),
        sizing_mode=load_position_sizing_mode(),
        plan_type=plan_type,
        options_enabled=bool(cfg.get("options_enabled")),
        live_order=_live_order(),
        live_trading=bool(cfg.get("live_trading")) or _env_bool("LIVE_TRADING_ENABLED", False),
        active_count=active,
        max_active=_max_active(),
        show_perp_options=_show_perp_options(),
        show_options_options=_show_options_options(),
        mutual_exclusive=mutual,
        has_standalone_option=has_standalone,
    )


def _gates_public(cfg: dict[str, Any], plan_type: str) -> dict[str, Any]:
    g = _gates_dict(cfg, plan_type)
    g["oo_close_mode_enabled"] = _oo_close_mode_enabled()
    g["oo_close_mode_default"] = "close_all" if _oo_close_mode_enabled() else "hold_expiry"
    g["oo_bias_split_by"] = _oo_bias_split_by()
    g["oo_bias_ratio"] = _oo_bias_ratio()
    g["budget_buffer"] = float(cfg.get("budget_buffer") or 0.95)
    return g


def _maybe_start_monitor(cfg: dict[str, Any]) -> None:
    # 始终启动监控线程:单独期权模式下仍需收口遗留 active/partial 计划
    with _hedge_start_lock():
        if cfg.get("hedge_monitor_thread") is not None:
            return
        try:
            secs = float(os.getenv("HEDGE_PLAN_MONITOR_POLL_SECONDS") or "15")
        except ValueError:
            secs = 15.0
        secs = max(5.0, secs)

        def _loop() -> None:
            import time

            from lib.hedge_plan.hedge_plan_monitor_lib import tick_active_plans

            while True:
                try:
                    tick_active_plans(cfg)
                except Exception:
                    pass
                time.sleep(secs)

        import threading

        t = threading.Thread(target=_loop, name="hedge-plan-monitor", daemon=True)
        t.start()
        cfg["hedge_monitor_thread"] = t


_start_lock = None


def _hedge_start_lock():
    global _start_lock
    if _start_lock is None:
        import threading

        _start_lock = threading.Lock()
    return _start_lock


def _start_body_json(body: dict[str, Any], missing_leg: Optional[str] = None) -> str:
    import json

    try:
        return json.dumps(
            {"start_body": body, "missing_leg": missing_leg},
            ensure_ascii=False,
        )[:8000]
    except Exception:
        return ""


def _persist_po(cfg: dict[str, Any], result: dict[str, Any], body: dict[str, Any]) -> int:
    from lib.hedge_plan.hedge_plan_db import (
        get_plan,
        get_plan_legs,
        init_hedge_plan_tables,
        insert_leg,
        insert_plan,
    )
    from lib.hedge_plan.hedge_plan_notify_lib import notify_plan_start

    conn = cfg["get_db"]()
    try:
        init_hedge_plan_tables(conn)
        is_partial = bool(result.get("partial"))
        missing = str(result.get("missing_leg") or "") if is_partial else ""
        opt = result.get("option") or {}
        perp = result.get("perp") or {}
        if is_partial:
            opt_ok = missing != "option_hedge" and bool(result.get("option"))
            perp_ok = missing != "perp" and bool(result.get("perp"))
        else:
            opt_ok = True
            perp_ok = True
        premium = float((opt or {}).get("premium") or 0) if opt_ok else 0.0
        from lib.hedge_plan.hedge_plan_option_primary_lib import (
            is_option_primary,
            perp_direction_for_view,
        )

        opt_primary = is_option_primary(body)
        view = str(body.get("direction") or "long")
        perp_dir = (
            str((perp or {}).get("direction") or "")
            or (perp_direction_for_view(view) if opt_primary else view)
        )
        plan_row = {
            "plan_type": "perp_options",
            "status": "partial" if is_partial else "active",
            "underlying": str(body.get("underlying") or "ETH").upper(),
            "direction": view,
            "entry_mark": float(body.get("entry") or 0),
            "tp": float(body.get("tp") or 0) if not opt_primary else 0,
            "sl": float(body.get("sl") or 0) if not opt_primary else 0,
            "sizing_mode_at_open": load_position_sizing_mode(),
            "perp_size": float((perp or {}).get("contracts") or body.get("contracts") or 0),
            "margin": body.get("margin"),
            "leverage": float(body.get("leverage") or (100 if opt_primary else 10)),
            "premium_total": premium,
            "preview_json": _start_body_json(body, missing or None),
            "close_reason": "partial_fail" if is_partial else None,
            "opened_at": result.get("opened_at"),
            "note": (result.get("msg") or "")[:500] if is_partial else None,
            "option_primary": 1 if opt_primary else 0,
            "perp_direction": perp_dir,
        }
        if opt_primary:
            plan_row.update(
                {
                    "option_target_points": float(body.get("option_target_points") or 0),
                    "perp_target_points": float(body.get("perp_target_points") or 0),
                    "option_perp_ratio": float(body.get("option_perp_ratio") or 0),
                    "premium_budget": float(body.get("premium_budget") or 0),
                    "strike_interval": float(body.get("strike_interval") or 15),
                    "min_option_hours": float(body.get("min_option_hours") or 36),
                    "option_moneyness": str(body.get("moneyness") or body.get("option_moneyness") or ""),
                }
            )
        plan_id = insert_plan(conn, plan_row)
        insert_leg(
            conn,
            {
                "plan_id": plan_id,
                "leg_role": "perp",
                "symbol": str(body.get("exchange_symbol") or ""),
                "side": perp_dir,
                "size": float((perp or {}).get("contracts") or body.get("contracts") or 0),
                "avg_open": float(body.get("entry") or 0) if perp_ok else None,
                "status": "open" if perp_ok else "pending",
                "exchange_ord_id": str((perp or {}).get("exchange_ord_id") or ""),
                "opened_at": result.get("opened_at") if perp_ok else None,
            },
        )
        insert_leg(
            conn,
            {
                "plan_id": plan_id,
                "leg_role": "option_hedge",
                "inst_id": str((opt or {}).get("inst_id") or body.get("opt_inst_id") or ""),
                "opt_type": str((opt or {}).get("opt_type") or body.get("opt_type") or ""),
                "strike": (opt or {}).get("strike") or body.get("strike"),
                "side": "buy",
                "size": float((opt or {}).get("sheets") or body.get("sheets") or 1),
                "avg_open": float((opt or {}).get("ask") or body.get("ask") or 0) if opt_ok else None,
                "premium": premium if opt_ok else 0,
                "ct_mult": float(body.get("ct_mult") or (opt or {}).get("ct_mult") or 0.01),
                "status": "open" if opt_ok else "pending",
                "exchange_ord_id": str((opt or {}).get("exchange_ord_id") or ""),
                "opened_at": result.get("opened_at") if opt_ok else None,
            },
        )
        conn.commit()
        if not is_partial:
            plan = get_plan(conn, plan_id)
            legs = get_plan_legs(conn, plan_id)
            if plan:
                notify_plan_start(cfg, conn, plan, legs)
                conn.commit()
        return plan_id
    finally:
        conn.close()


def _persist_po_watching(cfg: dict[str, Any], body: dict[str, Any]) -> int:
    """以期权为主:只落库盯盘计划,不下单."""
    from lib.hedge_plan.hedge_plan_db import init_hedge_plan_tables, insert_plan
    from lib.hedge_plan.hedge_plan_option_primary_lib import perp_direction_for_view

    conn = cfg["get_db"]()
    try:
        init_hedge_plan_tables(conn)
        view = str(body.get("direction") or "long")
        money = str(body.get("moneyness") or body.get("option_moneyness") or "otm").strip().lower()
        plan_id = insert_plan(
            conn,
            {
                "plan_type": "perp_options",
                "status": "watching",
                "underlying": str(body.get("underlying") or "ETH").upper(),
                "direction": view,
                "entry_mark": float(body.get("index_px") or body.get("entry") or 0) or None,
                "tp": 0,
                "sl": 0,
                "sizing_mode_at_open": None,
                "perp_size": None,
                "margin": None,
                "leverage": float(body.get("leverage") or 100),
                "premium_total": 0,
                "preview_json": _start_body_json(body),
                "close_reason": None,
                "opened_at": None,
                "note": "盯盘中:等待杠杆/间隔达标后自动开仓",
                "option_primary": 1,
                "perp_direction": perp_direction_for_view(view),
                "option_target_points": float(body.get("option_target_points") or 0),
                "perp_target_points": float(body.get("perp_target_points") or 0),
                "option_perp_ratio": float(body.get("option_perp_ratio") or 0),
                "premium_budget": float(body.get("premium_budget") or 0),
                "strike_interval": float(body.get("strike_interval") or 15),
                "min_option_hours": float(body.get("min_option_hours") or 36),
                "option_moneyness": money,
                "option_leverage": float(body.get("option_leverage") or 0),
            },
        )
        conn.commit()
        return plan_id
    finally:
        conn.close()


def _activate_watching_po(
    cfg: dict[str, Any],
    conn: Any,
    plan_id: int,
    result: dict[str, Any],
    body: dict[str, Any],
) -> None:
    """盯盘命中后:写入腿并把 watching → active/partial."""
    from lib.hedge_plan.hedge_plan_db import get_plan, get_plan_legs, insert_leg, update_plan
    from lib.hedge_plan.hedge_plan_notify_lib import notify_plan_start
    from lib.hedge_plan.hedge_plan_option_primary_lib import perp_direction_for_view

    is_partial = bool(result.get("partial"))
    missing = str(result.get("missing_leg") or "") if is_partial else ""
    opt = result.get("option") or {}
    perp = result.get("perp") or {}
    if is_partial:
        opt_ok = missing != "option_hedge" and bool(result.get("option"))
        perp_ok = missing != "perp" and bool(result.get("perp"))
    else:
        opt_ok = True
        perp_ok = True
    premium = float((opt or {}).get("premium") or 0) if opt_ok else 0.0
    view = str(body.get("direction") or "long")
    perp_dir = (
        str((perp or {}).get("direction") or "")
        or perp_direction_for_view(view)
    )
    update_plan(
        conn,
        int(plan_id),
        status="partial" if is_partial else "active",
        entry_mark=float(body.get("entry") or body.get("index_px") or 0) or None,
        perp_size=float((perp or {}).get("contracts") or body.get("contracts") or 0),
        leverage=float(body.get("leverage") or 100),
        premium_total=premium,
        preview_json=_start_body_json(body, missing or None),
        close_reason="partial_fail" if is_partial else None,
        opened_at=result.get("opened_at"),
        note=(result.get("msg") or "")[:500] if is_partial else "盯盘达标已开仓",
        perp_direction=perp_dir,
    )
    insert_leg(
        conn,
        {
            "plan_id": int(plan_id),
            "leg_role": "perp",
            "symbol": str(body.get("exchange_symbol") or ""),
            "side": perp_dir,
            "size": float((perp or {}).get("contracts") or body.get("contracts") or 0),
            "avg_open": float(body.get("entry") or 0) if perp_ok else None,
            "status": "open" if perp_ok else "pending",
            "exchange_ord_id": str((perp or {}).get("exchange_ord_id") or ""),
            "opened_at": result.get("opened_at") if perp_ok else None,
        },
    )
    insert_leg(
        conn,
        {
            "plan_id": int(plan_id),
            "leg_role": "option_hedge",
            "inst_id": str((opt or {}).get("inst_id") or body.get("opt_inst_id") or ""),
            "opt_type": str((opt or {}).get("opt_type") or body.get("opt_type") or ""),
            "strike": (opt or {}).get("strike") or body.get("strike"),
            "side": "buy",
            "size": float((opt or {}).get("sheets") or body.get("sheets") or 1),
            "avg_open": float((opt or {}).get("ask") or body.get("ask") or 0) if opt_ok else None,
            "premium": premium if opt_ok else 0,
            "ct_mult": float(body.get("ct_mult") or (opt or {}).get("ct_mult") or 0.01),
            "status": "open" if opt_ok else "pending",
            "exchange_ord_id": str((opt or {}).get("exchange_ord_id") or ""),
            "opened_at": result.get("opened_at") if opt_ok else None,
        },
    )
    if not is_partial:
        plan = get_plan(conn, int(plan_id))
        legs = get_plan_legs(conn, int(plan_id))
        if plan:
            notify_plan_start(cfg, conn, plan, legs)


def _persist_oo(cfg: dict[str, Any], result: dict[str, Any], body: dict[str, Any]) -> int:
    from lib.hedge_plan.hedge_plan_db import (
        get_plan,
        get_plan_legs,
        init_hedge_plan_tables,
        insert_leg,
        insert_plan,
    )
    from lib.hedge_plan.hedge_plan_notify_lib import notify_plan_start

    conn = cfg["get_db"]()
    try:
        init_hedge_plan_tables(conn)
        is_partial = bool(result.get("partial"))
        missing = str(result.get("missing_leg") or "") if is_partial else ""
        a = result.get("leg_a") or {}
        b = result.get("leg_b") or {}
        a_ok = True if not is_partial else bool(result.get("leg_a"))
        b_ok = True if not is_partial else (missing != "option_b" and bool(result.get("leg_b")))
        premium = (float(a.get("premium") or 0) if a_ok else 0.0) + (
            float(b.get("premium") or 0) if b_ok else 0.0
        )
        rr_raw = body.get("profit_rr")
        try:
            profit_rr = float(rr_raw) if rr_raw not in (None, "") else 2.0
        except (TypeError, ValueError):
            profit_rr = 2.0
        if profit_rr <= 0:
            profit_rr = 2.0
        # 旧字段兼容:不再要求上/下破;有传则原样落库
        def _opt_float(key: str, *alts: str) -> float | None:
            for k in (key, *alts):
                v = body.get(k)
                if v not in (None, ""):
                    try:
                        return float(v)
                    except (TypeError, ValueError):
                        continue
            return None

        up_f = _opt_float("target_price_up", "target_price")
        down_f = _opt_float("target_price_down", "target_price")
        plan_id = insert_plan(
            conn,
            {
                "plan_type": "options_options",
                "status": "partial" if is_partial else "active",
                "underlying": str(body.get("underlying") or "ETH").upper(),
                "target_price": up_f,
                "target_price_up": up_f,
                "target_price_down": down_f,
                "profit_rr": profit_rr,
                "sizing_mode_at_open": load_position_sizing_mode(),
                "premium_total": premium,
                "oo_close_mode": _normalize_oo_close_mode(body.get("oo_close_mode")),
                "preview_json": _start_body_json(body, missing or None),
                "close_reason": "partial_fail" if is_partial else None,
                "opened_at": result.get("opened_at"),
                "note": (result.get("msg") or "")[:500] if is_partial else None,
            },
        )
        for role, res, src, ok in (
            ("option_a", a, body.get("leg_a") or {}, a_ok),
            ("option_b", b, body.get("leg_b") or {}, b_ok),
        ):
            insert_leg(
                conn,
                {
                    "plan_id": plan_id,
                    "leg_role": role,
                    "inst_id": str((res or {}).get("inst_id") or src.get("inst_id") or ""),
                    "opt_type": str((res or {}).get("opt_type") or src.get("opt_type") or ""),
                    "strike": (res or {}).get("strike") or src.get("strike"),
                    "side": "buy",
                    "size": float((res or {}).get("sheets") or src.get("sheets") or 1),
                    "avg_open": float((res or {}).get("ask") or 0) if ok else None,
                    "premium": float((res or {}).get("premium") or 0) if ok else 0,
                    "status": "open" if ok else "pending",
                    "exchange_ord_id": str((res or {}).get("exchange_ord_id") or ""),
                    "opened_at": result.get("opened_at") if ok else None,
                },
            )
        conn.commit()
        if not is_partial:
            plan = get_plan(conn, plan_id)
            legs = get_plan_legs(conn, plan_id)
            if plan:
                notify_plan_start(cfg, conn, plan, legs)
                conn.commit()
        return plan_id
    finally:
        conn.close()


def register_hedge_plan_routes(app: Flask, cfg: dict[str, Any]) -> None:
    lr = cfg["login_required"]

    @app.route("/hedge-plan")
    @lr
    def page_hedge_plan():
        from lib.instance.instance_embed_lib import redirect_to_embed_shell_if_enabled

        redir = redirect_to_embed_shell_if_enabled("hedge_plan")
        if redir is not None:
            return redir
        return cfg["render_main_page"]("hedge_plan")

    @app.route("/api/hedge-plan/gates")
    @lr
    def api_hedge_gates():
        plan_type = (request.args.get("plan_type") or "perp_options").strip()
        return jsonify({"ok": True, **_gates_public(cfg, plan_type)})

    @app.route("/api/hedge-plan/market")
    @lr
    def api_hedge_market():
        base = (request.args.get("base") or cfg.get("default_underly") or "ETH").strip().upper()
        if base not in ("BTC", "ETH"):
            return jsonify({"ok": False, "msg": "对冲计划仅支持 BTC/ETH"}), 400
        direction = (request.args.get("direction") or "long").strip().lower()
        if direction not in ("long", "short"):
            direction = "long"
        option_primary = (request.args.get("option_primary") or "").strip().lower() in (
            "1",
            "true",
            "yes",
            "on",
        )
        data, err = _fetch_perp_market(cfg, base)
        if err:
            return jsonify({"ok": False, "msg": err}), 400
        sizing_mode = load_position_sizing_mode()
        gates = _gates_dict(cfg, "perp_options")
        if option_primary:
            from lib.hedge_plan.hedge_plan_option_primary_lib import (
                opt_type_for_view,
                perp_direction_for_view,
            )

            suggested = opt_type_for_view(direction)
            perp_dir = perp_direction_for_view(direction)
            acct_note = "以期权为主:看法腿买期权,永续反向对冲"
        else:
            suggested = "P" if direction == "long" else "C"
            perp_dir = direction
            acct_note = "永续腿使用合约(交易)账户可用 USDT"
        out = {
            "ok": True,
            "base": base,
            "direction": direction,
            "option_primary": option_primary,
            "suggested_opt_type": suggested,
            "perp_direction": perp_dir,
            **data,
            "gates": gates,
            "sizing_mode": sizing_mode,
            "account_kind": "perp",
            "account_label": cfg.get("perp_account_label") or "合约账户",
            "account_note": acct_note,
        }
        return jsonify(out)

    @app.route("/api/hedge-plan/options-chain")
    @lr
    def api_hedge_options_chain():
        if not cfg.get("options_enabled"):
            return jsonify({"ok": False, "msg": "期权模块未启用"}), 400
        ex = cfg.get("exchange_options")
        if ex is None:
            return jsonify({"ok": False, "msg": "期权交易所未初始化"}), 400
        u = (request.args.get("underlying") or cfg.get("default_underly") or "ETH").upper()
        # 热更新:链展示天数每次读 env
        chain_max_dte = float(
            os.getenv("OKX_OPTIONS_CHAIN_MAX_DTE_DAYS")
            or os.getenv("OKX_OPTIONS_MAX_DTE_DAYS")
            or cfg.get("chain_max_dte")
            or 14
        )
        try:
            chain = cfg["build_option_chain"](
                ex,
                u,
                max_dte_days=chain_max_dte,
                itm_only=False,
                itm_max_dist_usd=float(os.getenv("OKX_OPTIONS_ITM_MAX_DIST_USD") or "30"),
            )
        except Exception as e:
            return jsonify({"ok": False, "msg": f"拉取期权链失败: {e}"}), 500
        # 可选:永期以期权为主时按最低剩余小时/行权间隔过滤(仅当请求显式带 option_primary)
        # 默认拉链不再带此过滤,避免期期看不到明天到期
        option_primary = (request.args.get("option_primary") or "").strip().lower() in (
            "1",
            "true",
            "yes",
            "on",
        )
        min_hours = None
        strike_interval = None
        try:
            if request.args.get("min_hours") not in (None, ""):
                min_hours = float(request.args.get("min_hours"))
        except (TypeError, ValueError):
            min_hours = 36.0 if option_primary else None
        try:
            if request.args.get("strike_interval") not in (None, ""):
                strike_interval = float(request.args.get("strike_interval"))
        except (TypeError, ValueError):
            strike_interval = 15.0 if option_primary else None
        if option_primary and min_hours is None:
            min_hours = 36.0
        if option_primary and strike_interval is None:
            strike_interval = 15.0
        if min_hours is not None or strike_interval is not None:
            from lib.hedge_plan.hedge_plan_option_primary_lib import hours_to_expiry_from_ms

            idx = None
            try:
                idx = float(chain.get("index_px") or 0) or None
            except (TypeError, ValueError):
                idx = None
            filtered = []
            for exp in chain.get("expiries") or []:
                h = hours_to_expiry_from_ms(exp.get("exp_time"))
                if min_hours is not None and h is not None and h < min_hours:
                    continue
                contracts = []
                for c in exp.get("contracts") or []:
                    row = dict(c)
                    row["hours_to_expiry"] = h
                    if strike_interval is not None and idx and idx > 0:
                        try:
                            k = float(row.get("strike") or 0)
                        except (TypeError, ValueError):
                            k = 0.0
                        if k > 0 and abs(k - idx) > strike_interval + 1e-9:
                            continue
                    contracts.append(row)
                if contracts:
                    filtered.append({**exp, "contracts": contracts, "hours_to_expiry": h})
            chain = {**chain, "expiries": filtered}
        opt_acct = _options_account_snapshot(cfg)
        return jsonify(
            {
                "ok": True,
                **chain,
                "underlying": u,
                "chain_max_dte_days": chain_max_dte,
                "account_kind": "options",
                "account_label": cfg.get("options_account_label") or "期权账户",
                "account_note": "期权腿使用期权账户(交易 USDC)",
                "options_account": opt_acct,
                "trade_budget_usdc": cfg.get("trade_budget_usdc"),
                "budget_buffer": cfg.get("budget_buffer"),
                "option_primary": option_primary,
                "min_hours": min_hours,
                "strike_interval": strike_interval,
            }
        )

    @app.route("/api/hedge-plan/preview", methods=["POST"])
    @lr
    def api_hedge_preview():
        body = request.get_json(silent=True) or {}
        plan_type = (body.get("plan_type") or "perp_options").strip().lower()
        gates = _gates_dict(cfg, plan_type)
        if not gates.get("can_preview"):
            return jsonify({"ok": False, "msg": "; ".join(gates.get("reasons") or ["不可测算"]), "gates": gates}), 400
        try:
            if plan_type == "options_options":
                data = _preview_oo(body)
            else:
                data = _preview_po(body)
        except ValueError as e:
            return jsonify({"ok": False, "msg": str(e)}), 400
        except Exception as e:
            return jsonify({"ok": False, "msg": f"测算失败: {e}"}), 500
        return jsonify({"ok": True, "gates": gates, **data})

    @app.route("/api/hedge-plan/validate-path", methods=["POST"])
    @lr
    def api_hedge_validate_path():
        """只校验下单路径(强制 dry_run),不真实成交."""
        from lib.hedge_plan.hedge_plan_orders_lib import (
            execute_options_options_start,
            execute_perp_options_start,
            validate_start_body,
        )

        body = request.get_json(silent=True) or {}
        plan_type = (body.get("plan_type") or "perp_options").strip().lower()
        err = validate_start_body(plan_type, body)
        if err:
            return jsonify({"ok": False, "msg": err}), 400
        if plan_type == "options_options":
            out = execute_options_options_start(cfg, body, dry_run=True)
        else:
            out = execute_perp_options_start(cfg, body, dry_run=True)
        return jsonify(out), (200 if out.get("ok") else 400)

    @app.route("/api/hedge-plan/start", methods=["POST"])
    @lr
    def api_hedge_start():
        from lib.hedge_plan.hedge_plan_orders_lib import (
            execute_options_options_start,
            execute_perp_options_start,
            validate_start_body,
        )

        body = request.get_json(silent=True) or {}
        plan_type = (body.get("plan_type") or "perp_options").strip().lower()
        dry_run = bool(body.get("dry_run")) or _env_bool("HEDGE_PLAN_DRY_RUN", False)
        try:
            from lib.options.options_margin_mode_lib import is_coin_margin_mode

            if is_coin_margin_mode() and not dry_run:
                return jsonify(
                    {
                        "ok": False,
                        "msg": "当前单笔期权为币本位模式,对冲计划仅支持 USDC 期权;请将 OKX_OPTIONS_MARGIN_MODE=usdc 并重启后再开对冲",
                    }
                ), 400
        except Exception as e:
            return jsonify(
                {"ok": False, "msg": f"期权本位校验失败,已拒绝开对冲: {e}"}
            ), 400
        with _hedge_start_lock():
            gates = _gates_dict(cfg, plan_type)
            if not dry_run and not gates.get("can_start"):
                return jsonify(
                    {"ok": False, "msg": "; ".join(gates.get("reasons") or ["不可开仓"]), "gates": gates}
                ), 400
            err = validate_start_body(plan_type, body)
            if err:
                return jsonify({"ok": False, "msg": err, "gates": gates}), 400
            # 补齐永续杠杆(以期权为主默认 100;保险模式 BTC/ETH 用 btc_leverage)
            if plan_type == "perp_options" and not body.get("leverage"):
                from lib.hedge_plan.hedge_plan_option_primary_lib import is_option_primary

                if is_option_primary(body):
                    body["leverage"] = 100
                else:
                    base = str(body.get("underlying") or "ETH").upper()
                    if base in ("BTC", "ETH"):
                        body["leverage"] = int(cfg.get("btc_leverage") or 10)
                    else:
                        body["leverage"] = int(cfg.get("alt_leverage") or 5)
            # 以期权为主:策略启动=盯盘,不现场开仓
            if plan_type == "perp_options":
                from lib.hedge_plan.hedge_plan_option_primary_lib import is_option_primary

                watch = body.get("watch_entry")
                watch_on = watch in (None, "", True, 1, "1", "true", "yes", "on")
                if is_option_primary(body) and watch_on:
                    if dry_run:
                        return jsonify(
                            {
                                "ok": True,
                                "dry_run": True,
                                "watching": True,
                                "msg": "dry_run:将创建盯盘计划(不落库)",
                                "gates": gates,
                            }
                        )
                    plan_id = _persist_po_watching(cfg, body)
                    return jsonify(
                        {
                            "ok": True,
                            "watching": True,
                            "plan_id": plan_id,
                            "msg": "已启动盯盘,杠杆/间隔达标后自动开仓",
                            "gates": gates,
                        }
                    )
            if plan_type == "options_options":
                out = execute_options_options_start(
                    cfg,
                    body,
                    dry_run=dry_run,
                    persist=(None if dry_run else (lambda r, b: _persist_oo(cfg, r, b))),
                )
            else:
                out = execute_perp_options_start(
                    cfg,
                    body,
                    dry_run=dry_run,
                    persist=(None if dry_run else (lambda r, b: _persist_po(cfg, r, b))),
                )
            out["gates"] = gates
            return jsonify(out), (200 if out.get("ok") else 400)

    @app.route("/api/hedge-plan/<int:plan_id>/end", methods=["POST"])
    @lr
    def api_hedge_end_plan(plan_id: int):
        """人工结束进行中计划:不自动平仓;未成交腿改为 cancelled."""
        from lib.hedge_plan.hedge_plan_db import init_hedge_plan_tables
        from lib.hedge_plan.hedge_plan_orders_lib import execute_manual_end_plan

        conn = cfg["get_db"]()
        try:
            init_hedge_plan_tables(conn)
            out = execute_manual_end_plan(cfg, conn, plan_id)
            if not out.get("ok"):
                return jsonify(out), 400
            conn.commit()
        finally:
            conn.close()
        return jsonify(out)

    @app.route("/api/hedge-plan/<int:plan_id>/complete-leg", methods=["POST"])
    @lr
    def api_hedge_complete_leg(plan_id: int):
        """半腿待补:手动补开缺失腿,成功后升为 active."""
        import json

        from lib.hedge_plan.hedge_plan_db import (
            get_plan,
            get_plan_legs,
            init_hedge_plan_tables,
            update_leg,
            update_plan,
        )
        from lib.hedge_plan.hedge_plan_notify_lib import notify_plan_start
        from lib.hedge_plan.hedge_plan_orders_lib import execute_complete_missing_leg

        body = request.get_json(silent=True) or {}
        dry_run = bool(body.get("dry_run")) or _env_bool("HEDGE_PLAN_DRY_RUN", False)
        if not dry_run and not _hedge_enabled():
            return jsonify({"ok": False, "msg": "当前交易模式为单独期权,不可补开对冲腿"}), 400
        conn = cfg["get_db"]()
        try:
            init_hedge_plan_tables(conn)
            plan = get_plan(conn, plan_id)
            if not plan:
                return jsonify({"ok": False, "msg": "计划不存在"}), 404
            pt = str(plan.get("plan_type") or "")
            if pt == "perp_options" and not _show_perp_options():
                return jsonify({"ok": False, "msg": "当前模式非永期对冲,不可补开"}), 400
            if pt == "options_options" and not _show_options_options():
                return jsonify({"ok": False, "msg": "当前模式非期期对冲,不可补开"}), 400
            if str(plan.get("status") or "") != "partial":
                return jsonify({"ok": False, "msg": "仅半腿待补(partial)计划可补开"}), 400
            legs = get_plan_legs(conn, plan_id)
            start_body: dict[str, Any] = {}
            try:
                meta = json.loads(plan.get("preview_json") or "{}")
                if isinstance(meta, dict):
                    start_body = dict(meta.get("start_body") or {})
            except Exception:
                start_body = {}
            if not start_body:
                return jsonify({"ok": False, "msg": "缺少开仓参数,无法补开"}), 400
            # 允许请求体覆盖少量字段
            for k in ("contracts", "leverage", "sheets", "tp", "sl"):
                if body.get(k) not in (None, ""):
                    start_body[k] = body.get(k)
            out = execute_complete_missing_leg(
                cfg, plan, legs, start_body, dry_run=dry_run
            )
            if not out.get("ok"):
                return jsonify(out), 400
            if dry_run:
                return jsonify(out)
            fill = out.get("fill") or {}
            leg_id = out.get("leg_id")
            role = str(out.get("leg_role") or "")
            opened_at = out.get("opened_at")
            if leg_id:
                if role == "perp":
                    update_leg(
                        conn,
                        int(leg_id),
                        status="open",
                        size=float(fill.get("contracts") or start_body.get("contracts") or 0),
                        avg_open=float(start_body.get("entry") or plan.get("entry_mark") or 0),
                        exchange_ord_id=str(fill.get("exchange_ord_id") or ""),
                        opened_at=opened_at,
                    )
                    update_plan(
                        conn,
                        plan_id,
                        status="active",
                        close_reason=None,
                        note=None,
                        perp_size=float(fill.get("contracts") or start_body.get("contracts") or 0),
                    )
                else:
                    prem = float(fill.get("premium") or 0)
                    update_leg(
                        conn,
                        int(leg_id),
                        status="open",
                        size=float(fill.get("sheets") or start_body.get("sheets") or 1),
                        avg_open=float(fill.get("ask") or 0),
                        premium=prem,
                        exchange_ord_id=str(fill.get("exchange_ord_id") or ""),
                        opened_at=opened_at,
                        inst_id=str(fill.get("inst_id") or ""),
                    )
                    old_prem = float(plan.get("premium_total") or 0)
                    update_plan(
                        conn,
                        plan_id,
                        status="active",
                        close_reason=None,
                        note=None,
                        premium_total=old_prem + prem,
                    )
            conn.commit()
            plan2 = get_plan(conn, plan_id)
            legs2 = get_plan_legs(conn, plan_id)
            if plan2:
                notify_plan_start(cfg, conn, plan2, legs2)
                conn.commit()
            out["plan_id"] = plan_id
            out["status"] = "active"
            out["plan"] = plan2
            out["legs"] = legs2
            return jsonify(out)
        finally:
            conn.close()

    @app.route("/api/hedge-plan/list")
    @lr
    def api_hedge_list():
        from lib.hedge_plan.hedge_plan_db import init_hedge_plan_tables, list_plans

        status = (request.args.get("status") or "").strip() or None
        plan_type = (request.args.get("plan_type") or "").strip() or None
        underlying = (request.args.get("underlying") or "").strip() or None
        conn = cfg["get_db"]()
        try:
            init_hedge_plan_tables(conn)
            rows = list_plans(
                conn, status=status, plan_type=plan_type, underlying=underlying, limit=80
            )
            conn.commit()
        finally:
            conn.close()
        return jsonify({"ok": True, "plans": rows})

    @app.route("/api/hedge-plan/history")
    @lr
    def api_hedge_history():
        from lib.hedge_plan.hedge_plan_db import (
            attach_legs_to_plans,
            init_hedge_plan_tables,
            list_plans,
        )

        conn = cfg["get_db"]()
        try:
            init_hedge_plan_tables(conn)
            rows = list_plans(conn, status="closed", limit=100)
            failed = list_plans(conn, status="failed", limit=50)
            cancelled = list_plans(conn, status="cancelled", limit=50)
            merged = attach_legs_to_plans(conn, rows + failed + cancelled)
            conn.commit()
        finally:
            conn.close()
        return jsonify({"ok": True, "plans": merged})

    @app.route("/api/hedge-plan/active")
    @lr
    def api_hedge_active():
        from lib.hedge_plan.hedge_plan_db import (
            attach_legs_to_plans,
            init_hedge_plan_tables,
            list_plans,
        )
        from lib.hedge_plan.hedge_plan_orders_lib import reconcile_unfilled_option_legs

        conn = cfg["get_db"]()
        try:
            init_hedge_plan_tables(conn)
            rows = []
            for status in ("watching", "opening", "active", "partial"):
                rows.extend(list_plans(conn, status=status, limit=80))
            rows.sort(key=lambda row: int(row.get("id") or 0), reverse=True)
            for row in rows:
                if str(row.get("status") or "") == "watching":
                    continue
                try:
                    reconcile_unfilled_option_legs(cfg, conn, int(row["id"]))
                except Exception:
                    pass
            # 校正后可能 status 变化,重新拉一遍
            rows = []
            for status in ("watching", "opening", "active", "partial"):
                rows.extend(list_plans(conn, status=status, limit=80))
            rows.sort(key=lambda row: int(row.get("id") or 0), reverse=True)
            plans = attach_legs_to_plans(conn, rows)
            conn.commit()
        finally:
            conn.close()
        return jsonify({"ok": True, "plans": plans})

    @app.route("/api/hedge-plan/stats")
    @lr
    def api_hedge_stats():
        from lib.hedge_plan.hedge_plan_db import init_hedge_plan_tables, stats_summary

        conn = cfg["get_db"]()
        try:
            init_hedge_plan_tables(conn)
            s = stats_summary(conn)
            conn.commit()
        finally:
            conn.close()
        return jsonify({"ok": True, **s})

    @app.route("/api/hedge-plan/<int:plan_id>")
    @lr
    def api_hedge_detail(plan_id: int):
        from lib.hedge_plan.hedge_plan_db import (
            get_plan,
            get_plan_legs,
            init_hedge_plan_tables,
            legs_contract_summary,
        )
        from lib.hedge_plan.hedge_plan_orders_lib import reconcile_unfilled_option_legs

        conn = cfg["get_db"]()
        try:
            init_hedge_plan_tables(conn)
            plan = get_plan(conn, plan_id)
            if not plan:
                return jsonify({"ok": False, "msg": "计划不存在"}), 404
            # 打开细节时校正:无成交却标 open → cancelled
            if str(plan.get("status") or "") in ("opening", "active", "partial"):
                reconcile_unfilled_option_legs(cfg, conn, plan_id)
                plan = get_plan(conn, plan_id) or plan
            legs = get_plan_legs(conn, plan_id)
            conn.commit()
        finally:
            conn.close()
        return jsonify(
            {
                "ok": True,
                "plan": plan,
                "legs": legs,
                "contracts_summary": legs_contract_summary(legs),
            }
        )

    @app.route("/api/hedge-plan/<int:plan_id>", methods=["DELETE"])
    @lr
    def api_hedge_delete(plan_id: int):
        from lib.hedge_plan.hedge_plan_db import delete_plan, init_hedge_plan_tables

        conn = cfg["get_db"]()
        try:
            init_hedge_plan_tables(conn)
            out = delete_plan(conn, plan_id)
            if not out.get("ok"):
                return jsonify(out), 400
            conn.commit()
        finally:
            conn.close()
        return jsonify(out)

    @app.route("/api/hedge-plan/monitor-tick", methods=["POST"])
    @lr
    def api_hedge_monitor_tick():
        from lib.hedge_plan.hedge_plan_monitor_lib import tick_active_plans

        return jsonify(tick_active_plans(cfg))


def _preview_po(body: dict[str, Any]) -> dict[str, Any]:
    from lib.hedge_plan.hedge_plan_moneyness_lib import validate_po_option_moneyness
    from lib.hedge_plan.hedge_plan_option_primary_lib import (
        build_option_primary_preview,
        is_option_primary,
        size_from_premium,
        validate_option_primary_start,
    )

    if is_option_primary(body):
        err = validate_option_primary_start(body)
        if err:
            raise ValueError(err)
        sized = size_from_premium(
            premium_budget=float(body.get("premium_budget") or 0),
            ask=float(body.get("ask") or 0),
            ct_mult=float(body.get("ct_mult") or 0.01),
            ratio=float(body.get("option_perp_ratio") or 2),
            contract_size=float(body.get("contract_size") or 0.01),
        )
        if not sized.get("ok"):
            raise ValueError(sized.get("msg") or "定仓失败")
        body = dict(body)
        body["sheets"] = sized["sheets"]
        body["contracts"] = sized["contracts"]
        body["eth_qty"] = sized["eth_qty"]
        if not body.get("entry"):
            body["entry"] = body.get("index_px") or 0
        out = build_option_primary_preview(body)
        out["sizing"] = sized
        return out

    direction = str(body.get("direction") or "long").lower()
    entry = float(body["entry"])
    tp = float(body["tp"])
    sl = float(body["sl"])
    contracts = float(body["contracts"])
    contract_size = float(body.get("contract_size") or 0.01)
    opt_type = str(body.get("opt_type") or ("P" if direction == "long" else "C"))
    strike = float(body["strike"])
    sheets = float(body.get("sheets") or 1)
    ct_mult = float(body.get("ct_mult") or 0.01)
    ask = body.get("ask")
    premium = body.get("premium_paid")
    if premium is None:
        if ask is None:
            raise ValueError("缺少权利金或卖一价")
        premium = option_premium_total(ask=float(ask), sheets=sheets, ct_mult=ct_mult)
    index_px = body.get("index_px")
    idx_for_money = float(index_px) if index_px is not None else entry
    money_err = validate_po_option_moneyness(
        opt_type=opt_type,
        strike=strike,
        index_px=idx_for_money,
        ask=ask,
        hours_to_expiry=body.get("hours_to_expiry"),
    )
    if money_err:
        raise ValueError(money_err)
    return build_perp_options_preview(
        direction=direction,
        entry=entry,
        tp=tp,
        sl=sl,
        contracts=contracts,
        contract_size=contract_size,
        opt_type=opt_type,
        strike=strike,
        sheets=sheets,
        ct_mult=ct_mult,
        premium_paid=float(premium),
        index_px=float(index_px) if index_px is not None else None,
    )


def _preview_oo(body: dict[str, Any]) -> dict[str, Any]:
    from lib.hedge_plan.hedge_plan_moneyness_lib import validate_oo_legs_moneyness

    rr_raw = body.get("profit_rr")
    profit_rr = None
    if rr_raw not in (None, ""):
        profit_rr = float(rr_raw)
        if profit_rr <= 0:
            raise ValueError("盈亏比须大于0")
    up = body.get("target_price_up")
    down = body.get("target_price_down")
    legacy = body.get("target_price")
    if up in (None, "") and legacy not in (None, ""):
        up = legacy
    if down in (None, "") and legacy not in (None, ""):
        down = legacy
    if profit_rr is None and (up in (None, "") or down in (None, "")):
        raise ValueError("请填写盈亏比")
    up_f = float(up) if up not in (None, "") else None
    down_f = float(down) if down not in (None, "") else None
    if profit_rr is None and up_f is not None and down_f is not None and up_f <= down_f:
        raise ValueError("上破目标价必须大于下破目标价")
    index_px = body.get("index_px")
    if index_px in (None, ""):
        if up_f is not None and down_f is not None:
            index_px = (up_f + down_f) / 2
        else:
            raise ValueError("缺少指数价格")
    index_px = float(index_px)
    leg_a = body.get("leg_a") or {}
    leg_b = body.get("leg_b") or {}
    for name, leg in (("leg_a", leg_a), ("leg_b", leg_b)):
        if not leg.get("strike"):
            raise ValueError(f"缺少 {name} 行权价")
        if leg.get("premium_paid") is None and leg.get("ask") is not None:
            leg["premium_paid"] = option_premium_total(
                ask=float(leg["ask"]),
                sheets=float(leg.get("sheets") or 1),
                ct_mult=float(leg.get("ct_mult") or 0.01),
            )
        if leg.get("premium_paid") is None:
            raise ValueError(f"缺少 {name} 权利金")
    money_err = validate_oo_legs_moneyness(leg_a, leg_b, index_px=index_px)
    if money_err:
        raise ValueError(money_err)
    return build_options_options_preview(
        profit_rr=profit_rr,
        target_price_up=up_f,
        target_price_down=down_f,
        index_px=index_px,
        leg_a=leg_a,
        leg_b=leg_b,
    )


def _fetch_perp_market(cfg: dict[str, Any], base: str) -> tuple[dict[str, Any], str | None]:
    ex = cfg.get("exchange")
    if ex is None:
        return {}, "永续交易所未初始化"
    ensure = cfg.get("ensure_markets_loaded")
    if callable(ensure):
        try:
            ensure()
        except Exception as e:
            return {}, f"加载市场失败: {e}"
    norm = cfg.get("normalize_exchange_symbol")
    sym = f"{base}/USDT:USDT"
    if callable(norm):
        try:
            sym = norm(f"{base}/USDT")
        except Exception:
            sym = f"{base}/USDT:USDT"
    mark = bid = ask = last = None
    try:
        t = ex.fetch_ticker(sym)
        last = _sf(t.get("last"))
        mark = _sf(t.get("info", {}).get("markPx")) if isinstance(t.get("info"), dict) else None
        if mark is None:
            mark = _sf(t.get("mark")) or last
        bid = _sf(t.get("bid"))
        ask = _sf(t.get("ask"))
    except Exception as e:
        return {}, f"拉永续行情失败: {e}"

    cs = 0.01
    get_cs = cfg.get("get_contract_size")
    if callable(get_cs):
        try:
            cs = float(get_cs(sym) or 0.01)
        except Exception:
            cs = 0.01

    available = None
    get_av = cfg.get("get_available_trading_usdt")
    if callable(get_av):
        try:
            available = get_av()
        except Exception:
            available = None

    entry = float(mark or last or 0)
    sizing = None
    suggest_contracts = None
    amount_precision = 4
    try:
        amount_precision = int(amount_decimals_from_exchange(ex, sym))
    except Exception:
        amount_precision = 4
    if available is not None and entry > 0:
        sizing, _serr = compute_full_margin_sizing(
            symbol=sym,
            available_usdt=float(available),
            capital_base=float(available),
            buffer_ratio=float(cfg.get("full_margin_buffer") or 0.98),
            btc_leverage=int(cfg.get("btc_leverage") or 10),
            alt_leverage=int(cfg.get("alt_leverage") or 5),
            funds_decimals=int(cfg.get("funds_decimals") or 2),
        )
        if sizing:
            raw_contracts = suggest_contracts_from_notional(
                notional=float(sizing["notional_value"]),
                entry=entry,
                contract_size=cs,
            )
            # 优先走交易所 amount_to_precision;失败则按精度位数向下取整
            suggest_contracts = None
            try:
                precise = float(ex.amount_to_precision(sym, raw_contracts))
                if precise > raw_contracts + 1e-12:
                    precise = floor_contracts_to_precision(raw_contracts, amount_precision)
                suggest_contracts = precise
            except Exception:
                suggest_contracts = floor_contracts_to_precision(raw_contracts, amount_precision)

    return {
        "exchange_symbol": sym,
        "mark": mark,
        "last": last,
        "bid": bid,
        "ask": ask,
        "contract_size": cs,
        "available_usdt": available,
        "full_margin_sizing": sizing,
        "suggest_contracts": suggest_contracts,
        "amount_precision": amount_precision,
        "unit_quote": "USDT",
        "unit_contracts": "合约张",
        "unit_note": "价格单位 USDT;张数=交易所永续合约张(与下单精度一致);名义≈张数×面值×价格",
        "entry_ref": entry or None,
    }, None


def _options_account_snapshot(cfg: dict[str, Any]) -> dict[str, Any]:
    """期权账户资金快照(与期权页同源: exchange_options)."""
    out: dict[str, Any] = {
        "label": cfg.get("options_account_label") or "期权账户",
        "trading_usdc": None,
        "funding_usdc": None,
        "trading_usdt": None,
        "funding_usdt": None,
    }
    ex = cfg.get("exchange_options")
    hdr = cfg.get("options_header_balances")
    if ex is None or not callable(hdr):
        return out
    try:
        trading_usdc, funding_usdc, funding_usdt, trading_usdt = hdr(ex, force=False)
        out.update(
            {
                "trading_usdc": trading_usdc,
                "funding_usdc": funding_usdc,
                "trading_usdt": trading_usdt,
                "funding_usdt": funding_usdt,
            }
        )
    except Exception:
        pass
    return out


def _sf(v: Any) -> float | None:
    if v is None or v == "":
        return None
    try:
        return float(v)
    except (TypeError, ValueError):
        return None
