Wide order dialog (calc|form); enrich live cards with lev/bid/margin/time.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-10-14 16:45:05 +08:00
parent e6701563bd
commit b7b1a7e7e0
4 changed files with 213 additions and 36 deletions
@@ -1,15 +1,19 @@
"""永期对冲 · 当前持仓:期权+永续实时卡(组合净利扣开平双边手续费)."""
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:
@@ -33,19 +37,52 @@ def _pick_option_row(rows: list[dict[str, Any]] | None, inst_id: str) -> Optiona
return None
def _option_bid(ex: Any, inst_id: str, fallback: Optional[float]) -> Optional[float]:
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") or bids[0].get("price"))
bid = _sf(bids[0].get("px"))
sz = _sf(bids[0].get("sz"))
if bid is not None and bid > 0:
return bid
return bid, sz
except Exception:
pass
return fallback
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 build_live_hedge_cards(cfg: dict[str, Any], conn: Any) -> list[dict[str, Any]]:
@@ -79,6 +116,7 @@ def build_live_hedge_cards(cfg: dict[str, Any], conn: Any) -> list[dict[str, Any
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:
@@ -97,14 +135,21 @@ def build_live_hedge_cards(cfg: dict[str, Any], conn: Any) -> list[dict[str, Any
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 = _option_bid(ex_opt, inst_id, mark_opt) if inst_id else mark_opt
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()
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:
@@ -115,6 +160,7 @@ def build_live_hedge_cards(cfg: dict[str, Any], conn: Any) -> list[dict[str, Any
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"))
@@ -122,12 +168,15 @@ def build_live_hedge_cards(cfg: dict[str, Any], conn: Any) -> list[dict[str, Any
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):
@@ -153,19 +202,29 @@ def build_live_hedge_cards(cfg: dict[str, Any], conn: Any) -> list[dict[str, Any
contract_size=float(cs or 0.01),
fee=fr,
)
# 币本位期权净利(币) → USDT;永续净利已是 U
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"),
"underlying": uly,
"direction": view,
"opened_at": plan.get("opened_at"),
"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": {
@@ -177,6 +236,10 @@ def build_live_hedge_cards(cfg: dict[str, Any], conn: Any) -> list[dict[str, Any
"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),
@@ -188,6 +251,8 @@ def build_live_hedge_cards(cfg: dict[str, Any], conn: Any) -> list[dict[str, Any
"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),
+14 -15
View File
@@ -102,15 +102,16 @@
</div>
<div id="hp-opt-bal-line" class="muted hp-po-meta hp-opt-bal-line"></div>
<!-- 弹窗下单(挂到 body 时样式靠 .hp-po-order-dialog 自身,不依赖页 wrap) -->
<!-- 弹窗下单:宽屏左测算 / 右下单 -->
<div id="hp-order-panel-host" class="opt-order-backdrop" hidden aria-hidden="true">
<div id="hp-order-panel" class="opt-order-dialog hp-po-order-dialog" role="dialog" aria-modal="true" aria-labelledby="hp-order-dialog-title" style="display:none">
<div id="hp-order-panel" class="opt-order-dialog hp-po-order-dialog hp-po-order-dialog--wide" role="dialog" aria-modal="true" aria-labelledby="hp-order-dialog-title" style="display:none">
<div class="opt-order-dialog-head">
<h3 class="opt-order-title" id="hp-order-dialog-title">手动下单 · <span id="hp-perp-uly-label">ETH</span></h3>
<button type="button" class="hp-po-dialog-x" id="hp-order-close-btn" aria-label="关闭">×</button>
</div>
<div class="opt-order-layout">
<div class="opt-order-main">
<div class="opt-order-layout hp-po-order-split">
<aside class="hp-po-order-calc-pane" aria-label="仓位测算">
<h4 class="hp-po-section-title">仓位测算</h4>
<div class="hp-po-dialog-meta">
<div id="hp-sel-inst" class="options-order-inst">—</div>
<p class="muted hp-po-dialog-sub" id="hp-premium-line"></p>
@@ -118,10 +119,17 @@
<p id="hp-perp-quote" class="muted hp-po-dialog-sub" hidden>加载中…</p>
<p id="hp-po-perp-quote-right" class="muted" hidden></p>
</div>
<div class="hp-po-calc-box">
<p class="muted hp-po-max-line" id="hp-po-max-line">期权开仓最大 —</p>
<p class="muted hp-po-perp-auto" id="hp-po-perp-auto">永续开仓 —</p>
<div id="hp-perp-pnl-line" class="hp-po-pnl"></div>
<div id="hp-sizing-line" class="muted hp-po-sizing"></div>
</div>
<span id="hp-po-mode-badge" class="hp-po-mode-badge" hidden>手动下单</span>
<span id="hp-po-card-title" hidden>下单</span>
<span class="muted hp-acct-tag" id="hp-perp-acct-tag" hidden>合约账户</span>
</aside>
<div class="opt-order-main hp-po-order-form-pane">
<div class="hp-po-fields hidden" id="hp-po-fields-insurance" hidden>
<label class="hp-po-field">
<span class="hp-po-field-lab">开仓价 <em>USDT</em></span>
@@ -183,15 +191,6 @@
</div>
<p class="err hp-po-order-err" id="hp-po-order-err" hidden></p>
</section>
<details class="tip-collapse hp-rule-collapse hp-po-order-calc">
<summary class="tip-collapse-summary">仓位测算明细</summary>
<div class="tip-collapse-body">
<p class="muted hp-po-max-line" id="hp-po-max-line">期权开仓最大 —</p>
<p class="muted hp-po-perp-auto" id="hp-po-perp-auto">永续开仓 —</p>
<div id="hp-perp-pnl-line" class="hp-po-pnl"></div>
<div id="hp-sizing-line" class="muted hp-po-sizing"></div>
</div>
</details>
</div>
<div class="opt-order-dialog-actions">
<span id="hp-po-strategy-status" class="hp-po-strategy-status" aria-live="polite"></span>
@@ -432,4 +431,4 @@
</div>
</div>
</div>
<script src="/static/hedge_plan.js?v=54"></script>
<script src="/static/hedge_plan.js?v=55"></script>