"""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 _show_perp_options() -> bool: return _env_bool("HEDGE_PLAN_SHOW_PERP_OPTIONS", True) def _show_options_options() -> bool: return _env_bool("HEDGE_PLAN_SHOW_OPTIONS_OPTIONS", True) 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(), show_perp_options=_show_perp_options(), show_options_options=_show_options_options(), ) 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/") @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/", 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