"""永期对冲 · 当前持仓:期权+永续实时卡(组合净利扣开平双边手续费).""" from __future__ import annotations from datetime import datetime, timedelta, timezone from typing import Any, Optional from lib.hedge_plan.hedge_plan_option_primary_lib import ( estimate_combo_net_pnl, fee_rate, opt_type_for_view, option_bid_liquidity_ok, perp_direction_for_view, ) _BJ = timezone(timedelta(hours=8)) def _sf(v: Any) -> Optional[float]: try: if v is None or v == "": return None return float(v) except (TypeError, ValueError): return None def _pick_option_row(rows: list[dict[str, Any]] | None, inst_id: str) -> Optional[dict[str, Any]]: want = str(inst_id or "").strip() if not want or not rows: return None for r in rows: if not isinstance(r, dict): continue rid = str(r.get("instId") or r.get("inst_id") or "").strip() if rid == want: return r return None def _option_bid_quote( ex: Any, inst_id: str, fallback: Optional[float] ) -> tuple[Optional[float], Optional[float]]: """返回 (买一价, 买一张数).""" try: from lib.exchange.okx_options_lib import fetch_option_book_depth book = fetch_option_book_depth(ex, inst_id) or {} bids = book.get("bids") or [] if bids and isinstance(bids[0], dict): bid = _sf(bids[0].get("px")) sz = _sf(bids[0].get("sz")) if bid is not None and bid > 0: return bid, sz except Exception: pass return fallback, None def _parse_opt_exp_ms(inst_id: str) -> Optional[int]: """BTC-USD-261017-84000-P → 到期日 16:00 北京时间.""" parts = str(inst_id or "").upper().split("-") if len(parts) < 4: return None raw = parts[2] if len(raw) != 6 or not raw.isdigit(): return None try: yy, mm, dd = int(raw[0:2]), int(raw[2:4]), int(raw[4:6]) year = 2000 + yy dt = datetime(year, mm, dd, 16, 0, 0, tzinfo=_BJ) return int(dt.timestamp() * 1000) except Exception: return None def _parse_opened_ms(opened_at: Any) -> Optional[int]: s = str(opened_at or "").strip() if not s: return None try: # "2026-10-14 16:22:21" 按北京时间 dt = datetime.strptime(s[:19], "%Y-%m-%d %H:%M:%S").replace(tzinfo=_BJ) return int(dt.timestamp() * 1000) except Exception: return None def _pick_leg( legs: list[dict[str, Any]], roles: tuple[str, ...], *, prefer_open: bool = True ) -> Optional[dict[str, Any]]: matched = [x for x in legs if str(x.get("leg_role") or "") in roles] if not matched: return None if prefer_open: open_ones = [x for x in matched if str(x.get("status") or "").lower() == "open"] if open_ones: return open_ones[0] return matched[0] def _has_open_leg(legs: list[dict[str, Any]]) -> bool: return any(str(x.get("status") or "").lower() == "open" for x in legs) def build_live_hedge_cards(cfg: dict[str, Any], conn: Any) -> list[dict[str, Any]]: """当前持仓:未平完的永期仓(含计划已结束但腿仍 open).""" from lib.hedge_plan.hedge_plan_db import attach_legs_to_plans, list_plans_for_live_positions rows = list_plans_for_live_positions(conn, plan_type="perp_options", limit=40) if not rows: return [] plans = attach_legs_to_plans(conn, rows) fetch_opt = cfg.get("fetch_option_positions") ex_opt = None try: get_ex = cfg.get("exchange_options") ex_opt = get_ex() if callable(get_ex) else get_ex except Exception: ex_opt = None opt_rows: list[dict[str, Any]] = [] if callable(fetch_opt) and ex_opt is not None: try: opt_rows = fetch_opt(ex_opt) or [] except Exception: opt_rows = [] get_perp_metrics = cfg.get("get_live_position_exchange_metrics") get_cs = cfg.get("get_contract_size") fetch_idx = cfg.get("fetch_index_price") fr = fee_rate() now_ms = int(datetime.now(tz=_BJ).timestamp() * 1000) out: list[dict[str, Any]] = [] for plan in plans: legs = plan.get("legs") or [] plan_st = str(plan.get("status") or "").lower() # 已结束计划:仅当仍有未平腿才进当前持仓 if plan_st == "closed" and not _has_open_leg(legs): continue opt_leg = _pick_leg(legs, ("option_hedge", "option_a")) perp_leg = _pick_leg(legs, ("perp",)) if not opt_leg and not perp_leg: continue # 已结束:只展示仍 open 的腿;若该腿已平则置空 if plan_st == "closed": if opt_leg and str(opt_leg.get("status") or "").lower() != "open": opt_leg = None if perp_leg and str(perp_leg.get("status") or "").lower() != "open": perp_leg = None if not opt_leg and not perp_leg: continue view = str(plan.get("direction") or "long").lower() uly = str(plan.get("underlying") or "ETH").upper() inst_id = str((opt_leg or {}).get("inst_id") or "") opt_raw = _pick_option_row(opt_rows, inst_id) if inst_id else None sheets = _sf((opt_leg or {}).get("size")) or _sf((opt_raw or {}).get("pos")) or 0.0 ct = _sf((opt_leg or {}).get("ct_mult")) or 0.01 avg_open = _sf((opt_leg or {}).get("avg_open")) or _sf((opt_raw or {}).get("avgPx")) or 0.0 mark_opt = _sf((opt_raw or {}).get("markPx")) or _sf((opt_raw or {}).get("last")) or avg_open bid, bid_sz = _option_bid_quote(ex_opt, inst_id, mark_opt) if inst_id else (mark_opt, None) strike = _sf((opt_leg or {}).get("strike")) or 0.0 opt_type = str((opt_leg or {}).get("opt_type") or opt_type_for_view(view)).upper() # 币本位杠杆≈1÷卖一/标记 lev_ref = _sf(avg_open) or _sf(mark_opt) or 0.0 opt_leverage = round(1.0 / lev_ref, 1) if lev_ref and lev_ref > 0 else None liq_ok, liq_msg = option_bid_liquidity_ok(bid, bid_sz, need_sheets=float(sheets or 0)) sym = str((perp_leg or {}).get("symbol") or plan.get("exchange_symbol") or "") perp_dir = str( (perp_leg or {}).get("side") or plan.get("perp_direction") or perp_direction_for_view(view) ).lower() contracts = _sf((perp_leg or {}).get("size")) or _sf(plan.get("perp_size")) or 0.0 perp_entry = _sf((perp_leg or {}).get("avg_open")) or _sf(plan.get("entry_mark")) or 0.0 perp_lev = _sf(plan.get("leverage")) or 0.0 cs = 0.01 if callable(get_cs) and sym: try: cs = float(get_cs(sym) or 0.01) except Exception: cs = 0.01 perp_mark = None perp_upl = None live_contracts = None margin = None if callable(get_perp_metrics) and sym: try: m = get_perp_metrics(sym, perp_dir, order_leverage=plan.get("leverage")) if isinstance(m, dict): perp_mark = _sf(m.get("mark_price") or m.get("mark")) perp_upl = _sf(m.get("unrealized_pnl")) live_contracts = _sf(m.get("contracts") or m.get("size")) margin = _sf(m.get("initial_margin") or m.get("margin")) except Exception: pass if perp_mark is None or perp_mark <= 0: perp_mark = perp_entry if live_contracts and live_contracts > 0: contracts = live_contracts if (margin is None or margin <= 0) and contracts and perp_mark and perp_lev and perp_lev > 0: margin = round(float(contracts) * float(cs) * float(perp_mark) / float(perp_lev), 2) idx = None if callable(fetch_idx): try: idx = _sf(fetch_idx(uly)) except Exception: idx = None if idx is None or idx <= 0: idx = perp_mark or strike or 0.0 net = estimate_combo_net_pnl( view_side=view, strike=float(strike or 0), index_px=float(idx or 0), ask_open=float(avg_open or 0), bid=float(bid or 0), sheets=float(sheets or 0), ct_mult=float(ct or 0.01), perp_direction=perp_dir, perp_entry=float(perp_entry or 0), perp_mark=float(perp_mark or 0), contracts=float(contracts or 0), contract_size=float(cs or 0.01), fee=fr, ) opt_net_coin = float(net.get("opt_net") or 0) opt_net_usdt = round(opt_net_coin * float(idx or 0), 4) if idx else None perp_net = float(net.get("perp_net") or 0) combo_usdt = round((opt_net_usdt or 0) + perp_net, 4) if opt_net_usdt is not None else round(perp_net, 4) opened_at = plan.get("opened_at") opened_ms = _parse_opened_ms(opened_at) # 两腿开仓时间统一取计划 opened_at exp_ms = _parse_opt_exp_ms(inst_id) hold_sec = int((now_ms - opened_ms) / 1000) if opened_ms else None remain_sec = int((exp_ms - now_ms) / 1000) if exp_ms else None out.append( { "plan_id": plan.get("id"), "status": plan.get("status"), "unflat": plan_st == "closed", "underlying": uly, "direction": view, "opened_at": opened_at, "opened_ms": opened_ms, "exp_ms": exp_ms, "hold_sec": hold_sec, "remain_sec": remain_sec, "index_px": idx, "fee_rate": fr, "option": { "inst_id": inst_id or "—", "opt_type": opt_type, "strike": strike, "sheets": sheets, "ct_mult": ct, "avg_open": avg_open, "mark_px": mark_opt, "bid": bid, "bid_sz": bid_sz, "leverage": opt_leverage, "liq_ok": bool(liq_ok), "liq_msg": liq_msg or "", "net_coin": opt_net_coin, "net_usdt": opt_net_usdt, "live": bool(opt_raw), }, "perp": { "symbol": sym or "—", "side": perp_dir, "contracts": contracts, "contract_size": cs, "avg_open": perp_entry, "mark_px": perp_mark, "leverage": perp_lev or None, "margin": margin, "upl": perp_upl, "net_usdt": perp_net, "live": perp_upl is not None or (perp_mark is not None and perp_mark > 0), }, "combo": { "net_usdt": combo_usdt, "opt_net_usdt": opt_net_usdt, "perp_net_usdt": perp_net, "note": "扣开平双边手续费后估", }, } ) return out