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

import os
from typing import Any

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),
        "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(),
        "live_trading": _env_bool("LIVE_TRADING_ENABLED", False),
        "send_wechat": getattr(app_module, "send_wechat_msg", None),
    }


def _hedge_enabled() -> bool:
    return _env_bool("HEDGE_PLAN_ENABLED", False)


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
    try:
        from lib.hedge_plan.hedge_plan_db import count_active_plans, init_hedge_plan_tables

        conn = cfg["get_db"]()
        try:
            init_hedge_plan_tables(conn)
            active = count_active_plans(conn)
            conn.commit()
        finally:
            conn.close()
    except Exception:
        active = 0
    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(),
    )


def _maybe_start_monitor(cfg: dict[str, Any]) -> None:
    if not _hedge_enabled():
        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


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)
        opt = result.get("option") or {}
        perp = result.get("perp") or {}
        premium = float(opt.get("premium") or 0)
        plan_id = insert_plan(
            conn,
            {
                "plan_type": "perp_options",
                "status": "active",
                "underlying": str(body.get("underlying") or "ETH").upper(),
                "direction": str(body.get("direction") or "long"),
                "entry_mark": float(body.get("entry") or 0),
                "tp": float(body.get("tp") or 0),
                "sl": float(body.get("sl") or 0),
                "sizing_mode_at_open": load_position_sizing_mode(),
                "perp_size": float(perp.get("contracts") or body.get("contracts") or 0),
                "margin": body.get("margin"),
                "leverage": float(body.get("leverage") or 10),
                "premium_total": premium,
                "opened_at": result.get("opened_at"),
            },
        )
        insert_leg(
            conn,
            {
                "plan_id": plan_id,
                "leg_role": "perp",
                "symbol": str(body.get("exchange_symbol") or ""),
                "side": str(body.get("direction") or "long"),
                "size": float(perp.get("contracts") or body.get("contracts") or 0),
                "avg_open": float(body.get("entry") or 0),
                "status": "open",
                "exchange_ord_id": str(perp.get("exchange_ord_id") or ""),
                "opened_at": result.get("opened_at"),
            },
        )
        insert_leg(
            conn,
            {
                "plan_id": plan_id,
                "leg_role": "option_hedge",
                "inst_id": str(opt.get("inst_id") or body.get("opt_inst_id") or ""),
                "opt_type": str(opt.get("opt_type") or body.get("opt_type") or ""),
                "strike": opt.get("strike") or body.get("strike"),
                "side": "buy",
                "size": float(opt.get("sheets") or body.get("sheets") or 1),
                "avg_open": float(opt.get("ask") or 0),
                "premium": premium,
                "status": "open",
                "exchange_ord_id": str(opt.get("exchange_ord_id") or ""),
                "opened_at": result.get("opened_at"),
            },
        )
        conn.commit()
        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_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)
        a = result.get("leg_a") or {}
        b = result.get("leg_b") or {}
        premium = float(a.get("premium") or 0) + float(b.get("premium") or 0)
        plan_id = insert_plan(
            conn,
            {
                "plan_type": "options_options",
                "status": "active",
                "underlying": str(body.get("underlying") or "ETH").upper(),
                "target_price": float(
                    body.get("target_price_up")
                    or body.get("target_price")
                    or 0
                ),
                "target_price_up": float(
                    body.get("target_price_up")
                    or body.get("target_price")
                    or 0
                ),
                "target_price_down": float(
                    body.get("target_price_down")
                    or body.get("target_price")
                    or 0
                ),
                "sizing_mode_at_open": load_position_sizing_mode(),
                "premium_total": premium,
                "opened_at": result.get("opened_at"),
            },
        )
        for role, res, src in (("option_a", a, body.get("leg_a") or {}), ("option_b", b, body.get("leg_b") or {})):
            insert_leg(
                conn,
                {
                    "plan_id": plan_id,
                    "leg_role": role,
                    "inst_id": str(res.get("inst_id") or src.get("inst_id") or ""),
                    "opt_type": str(res.get("opt_type") or src.get("opt_type") or ""),
                    "strike": res.get("strike") or src.get("strike"),
                    "side": "buy",
                    "size": float(res.get("sheets") or src.get("sheets") or 1),
                    "avg_open": float(res.get("ask") or 0),
                    "premium": float(res.get("premium") or 0),
                    "status": "open",
                    "exchange_ord_id": str(res.get("exchange_ord_id") or ""),
                    "opened_at": result.get("opened_at"),
                },
            )
        conn.commit()
        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_dict(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"
        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")
        out = {
            "ok": True,
            "base": base,
            "direction": direction,
            "suggested_opt_type": "P" if direction == "long" else "C",
            **data,
            "gates": gates,
            "sizing_mode": sizing_mode,
            "account_kind": "perp",
            "account_label": cfg.get("perp_account_label") or "合约账户",
            "account_note": "永续腿使用合约(交易)账户可用 USDT",
        }
        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()
        try:
            chain = cfg["build_option_chain"](
                ex,
                u,
                max_dte_days=float(cfg.get("chain_max_dte") or 14),
                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
        opt_acct = _options_account_snapshot(cfg)
        return jsonify(
            {
                "ok": True,
                **chain,
                "underlying": u,
                "chain_max_dte_days": cfg.get("chain_max_dte"),
                "account_kind": "options",
                "account_label": cfg.get("options_account_label") or "期权账户",
                "account_note": "期权腿使用期权账户(交易 USDC)",
                "options_account": opt_acct,
            }
        )

    @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)
        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
        # 补齐永续杠杆
        if plan_type == "perp_options" and not body.get("leverage"):
            base = str(body.get("underlying") or "ETH").upper()
            body["leverage"] = cfg.get("btc_leverage") if base == "BTC" else (cfg.get("btc_leverage") or 10)
            # ETH 也用 BTC 档 10x 按方案;ALT 为 alt_leverage 仅非 BTC/ETH
            if base in ("BTC", "ETH"):
                body["leverage"] = int(cfg.get("btc_leverage") or 10)
        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/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,
        )

        conn = cfg["get_db"]()
        try:
            init_hedge_plan_tables(conn)
            rows = []
            for status in ("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,
        )

        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
            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]:
    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")
    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]:
    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 up in (None, "") or down in (None, ""):
        raise ValueError("请填写上破与下破目标价")
    up_f = float(up)
    down_f = float(down)
    if up_f <= down_f:
        raise ValueError("上破目标价必须大于下破目标价")
    index_px = float(body.get("index_px") or ((up_f + down_f) / 2))
    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} 权利金")
    return build_options_options_preview(
        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
