diff --git a/lib/common/static/hedge_plan.js b/lib/common/static/hedge_plan.js index 577597e..e41caa6 100644 --- a/lib/common/static/hedge_plan.js +++ b/lib/common/static/hedge_plan.js @@ -9,6 +9,7 @@ tab: "perp_options", mode: "perp_options", underlying: root.getAttribute("data-default-underly") || "ETH", + moneyFilter: "all", chain: null, selected: null, legA: null, @@ -34,10 +35,71 @@ return Number(v).toFixed(d == null ? 2 : d); } + function fmtOptionPx(v, tickSz) { + if (v === null || v === undefined || Number.isNaN(Number(v))) return "—"; + const n = Number(v); + const tick = Number(tickSz); + if (!tickSz || Number.isNaN(tick) || tick <= 0) { + return String(n).replace(/(\.\d*?[1-9])0+$/, "$1").replace(/\.0+$/, ""); + } + let decimals = 0; + if (tick < 1) decimals = Math.max(0, -Math.round(Math.log10(tick))); + else if (String(tick).indexOf(".") >= 0) decimals = String(tick).split(".")[1].length; + let s = n.toFixed(decimals).replace(/\.?0+$/, ""); + return s || "0"; + } + + /** 价格/流动性(张),价格按 tick_sz 对齐交易所精度. */ + function fmtPxSz(px, sz, estimated, tickSz) { + if (px === null || px === undefined || Number.isNaN(Number(px))) return "—"; + let price = fmtOptionPx(px, tickSz); + if (price === "—") return "—"; + if (estimated) price += "~"; + if (sz === null || sz === undefined || sz === "" || Number.isNaN(Number(sz))) return price; + const s = Number(sz); + const size = Math.abs(s - Math.round(s)) < 1e-9 ? String(Math.round(s)) : String(s); + return price + "/" + size; + } + + function moneynessBadge(c) { + const m = (c && c.moneyness) || ""; + const label = (c && c.moneyness_label) || "—"; + return '' + label + ""; + } + + function matchesMoneyFilter(c) { + const f = state.moneyFilter || "all"; + if (f === "all") return true; + const m = (c.moneyness || "").toLowerCase(); + if (f === "itm") return m === "itm" || m === "atm"; + if (f === "otm") return m === "otm"; + return true; + } + function optTypeForDirection(dir) { return dir === "short" ? "C" : "P"; } + function syncUnderlyingUI() { + const uly = state.underlying || "ETH"; + document.querySelectorAll(".hp-uly-btn, .hp-uly-btn-oo").forEach(function (b) { + const on = b.getAttribute("data-uly") === uly; + b.classList.toggle("active", on); + b.setAttribute("aria-pressed", on ? "true" : "false"); + }); + const lab = $("hp-perp-uly-label"); + if (lab) lab.textContent = uly; + const ooLab = $("hp-oo-uly-label"); + if (ooLab) ooLab.textContent = uly; + } + + function syncMoneyUI() { + document.querySelectorAll(".hp-money-btn").forEach(function (b) { + const on = b.getAttribute("data-money") === state.moneyFilter; + b.classList.toggle("active", on); + }); + } + function syncTabUI() { const tab = state.tab || "perp_options"; document.querySelectorAll(".hp-tab").forEach(function (b) { @@ -58,10 +120,6 @@ } } - function syncModeUI() { - syncTabUI(); - } - function setGateLine(gates) { const el = $("hp-gate-line"); if (!el) return; @@ -80,6 +138,23 @@ if (start) start.disabled = !gates.can_start; } + function setOptionsBalance(chain) { + const acct = (chain && chain.options_account) || {}; + const label = (chain && chain.account_label) || acct.label || "期权账户"; + const tag = $("hp-opt-acct-tag"); + if (tag) tag.textContent = label; + const line = + label + + " · 交易 USDC " + + fmt(acct.trading_usdc, 2) + + " · 资金 USDC " + + fmt(acct.funding_usdc, 2); + const el = $("hp-opt-bal-line"); + if (el) el.textContent = line; + const oo = $("hp-oo-bal-line"); + if (oo) oo.textContent = line; + } + async function loadGates() { try { const d = await apiJson("/api/hedge-plan/gates?plan_type=" + encodeURIComponent(state.mode)); @@ -99,10 +174,19 @@ ); state.market = d; setGateLine(d.gates); + const acctLabel = d.account_label || "合约账户"; + const tag = $("hp-perp-acct-tag"); + if (tag) tag.textContent = acctLabel; const q = $("hp-perp-quote"); if (q) { q.innerHTML = - "标记 " + + "" + + (d.base || state.underlying) + + " · " + + acctLabel + + "可用 " + + fmt(d.available_usdt, 2) + + " U
标记 " + fmt(d.mark, 2) + " · 最新 " + fmt(d.last, 2) + @@ -110,18 +194,17 @@ fmt(d.ask, 2) + " · 买一 " + fmt(d.bid, 2) + - "
面值 " + - fmt(d.contract_size, 4) + - " · 可用 " + - fmt(d.available_usdt, 2) + - " U"; + " · 面值 " + + fmt(d.contract_size, 4); } const sz = $("hp-sizing-line"); if (sz) { if (d.full_margin_sizing) { const s = d.full_margin_sizing; sz.textContent = - "全仓建议:保证金 " + + "全仓建议(" + + acctLabel + + "):保证金 " + fmt(s.margin_capital, 2) + "U × " + s.leverage + @@ -166,10 +249,15 @@ "/api/hedge-plan/options-chain?underlying=" + encodeURIComponent(state.underlying) ); state.chain = d; + const uly = d.underlying || state.underlying; const idx = $("hp-index-line"); - if (idx) idx.textContent = "指数 " + fmt(d.index_px, 2) + " · " + (d.inst_family || ""); + if (idx) { + idx.textContent = + "指数 " + uly + " " + fmt(d.index_px, 2) + " · " + (d.inst_family || "") + " · 实值含平值 · 虚值=价外"; + } const ooIdx = $("hp-oo-index"); - if (ooIdx) ooIdx.textContent = "指数 " + fmt(d.index_px, 2); + if (ooIdx) ooIdx.textContent = "指数 " + uly + " " + fmt(d.index_px, 2); + setOptionsBalance(d); fillExpSelect($("hp-exp-select"), d); fillExpSelect($("hp-oo-exp-select"), d); renderListStrikes(); @@ -194,13 +282,14 @@ const dir = ($("hp-direction") && $("hp-direction").value) || "long"; const want = optTypeForDirection(dir); const exp = currentExp("hp-exp-select"); + const prevInst = state.selected && state.selected.inst_id; tbody.innerHTML = ""; if (!exp) { tbody.innerHTML = '请选择到期日'; return; } const list = (exp.contracts || []).filter(function (c) { - return String(c.opt_type || "").toUpperCase() === want; + return String(c.opt_type || "").toUpperCase() === want && matchesMoneyFilter(c); }); if (!list.length) { tbody.innerHTML = '无匹配合约'; @@ -208,16 +297,21 @@ } list.forEach(function (c) { const tr = document.createElement("tr"); + tr.className = "opt-strike-row" + (c.moneyness ? " opt-row-" + c.moneyness : ""); + if (prevInst && c.inst_id === prevInst) tr.classList.add("opt-row-selected"); + tr.setAttribute("data-inst", c.inst_id); tr.innerHTML = "" + c.strike + "" + - c.opt_type + - "" + - fmt(c.ask, 4) + - "" + - fmt(c.bid, 4) + - ''; tbody.appendChild(tr); @@ -232,6 +326,12 @@ state.selected = c; const el = $("hp-sel-inst"); if (el) el.textContent = c.inst_id; + tbody.querySelectorAll(".opt-strike-row").forEach(function (r) { + r.classList.toggle("opt-row-selected", r.getAttribute("data-inst") === inst); + }); + tbody.querySelectorAll(".hp-pick").forEach(function (b) { + b.classList.toggle("active", b.getAttribute("data-inst") === inst); + }); updatePremiumLine(); }); }); @@ -247,7 +347,7 @@ const ct = Number(state.selected.ct_mult || 0.01); const ask = Number(state.selected.ask || 0); const prem = ask * sheets * ct; - line.textContent = "预估权利金 ≈ " + fmt(prem, 4) + " USDC"; + line.textContent = "预估权利金 ≈ " + fmt(prem, 4) + " USDC(期权账户)"; } function buildStraddleRows(contracts) { @@ -274,31 +374,37 @@ const exp = currentExp("hp-oo-exp-select"); tbody.innerHTML = ""; if (!exp) { - tbody.innerHTML = '请选择到期日'; + tbody.innerHTML = '请选择到期日'; return; } const rows = buildStraddleRows(exp.contracts); rows.forEach(function (row) { const tr = document.createElement("tr"); - const callAsk = row.call ? fmt(row.call.ask, 4) : "—"; - const putAsk = row.put ? fmt(row.put.ask, 4) : "—"; + const call = row.call; + const put = row.put; + const callAsk = call ? fmtPxSz(call.ask, call.ask_sz, call.ask_estimated, call.tick_sz) : "—"; + const putAsk = put ? fmtPxSz(put.ask, put.ask_sz, put.ask_estimated, put.tick_sz) : "—"; tr.innerHTML = - "" + + '' + callAsk + - '' + - (row.call + '' + + (call ? moneynessBadge(call) : "—") + + "" + + (call ? '' : "—") + '' + row.strike + - "" + + '
' + + (put ? moneynessBadge(put) : "—") + + '' + putAsk + - '' + - (row.put + "" + + (put ? '' : "—") + ""; @@ -334,8 +440,10 @@ c.opt_type + " K" + c.strike + + " " + + (c.moneyness_label || "") + " ask=" + - fmt(c.ask, 4) + + fmtPxSz(c.ask, c.ask_sz, c.ask_estimated, c.tick_sz) + " (" + c.inst_id + ")" @@ -355,6 +463,26 @@ }; } + function setUnderlying(uly, forceReload) { + const next = (uly || "ETH").toUpperCase(); + const changed = next !== state.underlying; + state.underlying = next; + syncUnderlyingUI(); + if (!changed && !forceReload) return; + state.selected = null; + state.legA = null; + state.legB = null; + if ($("hp-entry")) $("hp-entry").value = ""; + if ($("hp-contracts")) $("hp-contracts").value = ""; + if ($("hp-tp")) $("hp-tp").value = ""; + if ($("hp-sl")) $("hp-sl").value = ""; + if ($("hp-target")) $("hp-target").value = ""; + if ($("hp-sel-inst")) $("hp-sel-inst").textContent = "—"; + if ($("hp-premium-line")) $("hp-premium-line").textContent = ""; + if ($("hp-oo-legs")) $("hp-oo-legs").textContent = "尚未选用两腿"; + void refreshAll(); + } + async function runPreview() { const isOo = state.mode === "options_options"; const tbody = $(isOo ? "hp-result-tbody-oo" : "hp-result-tbody"); @@ -477,19 +605,21 @@ }); document.querySelectorAll(".hp-uly-btn, .hp-uly-btn-oo").forEach(function (b) { b.addEventListener("click", function () { - state.underlying = b.getAttribute("data-uly") || "ETH"; - document.querySelectorAll(".hp-uly-btn, .hp-uly-btn-oo").forEach(function (x) { - x.classList.toggle("active", x.getAttribute("data-uly") === state.underlying); - }); - state.selected = null; - state.legA = null; - state.legB = null; - void refreshAll(); + setUnderlying(b.getAttribute("data-uly") || "ETH", true); + }); + }); + document.querySelectorAll(".hp-money-btn").forEach(function (b) { + b.addEventListener("click", function () { + state.moneyFilter = b.getAttribute("data-money") || "all"; + syncMoneyUI(); + renderListStrikes(); }); }); const dir = $("hp-direction"); if (dir) { dir.addEventListener("change", function () { + state.selected = null; + if ($("hp-sel-inst")) $("hp-sel-inst").textContent = "—"; void loadMarket().then(function () { renderListStrikes(); }); @@ -541,6 +671,8 @@ } syncTabUI(); + syncUnderlyingUI(); + syncMoneyUI(); bind(); void refreshAll(); })(); diff --git a/lib/common/static/instance_theme.css b/lib/common/static/instance_theme.css index b4e6b56..216cf8d 100644 --- a/lib/common/static/instance_theme.css +++ b/lib/common/static/instance_theme.css @@ -3169,6 +3169,18 @@ html[data-theme="light"] .opt-be-dist-down { } /* 对冲计划 Tab:高对比选中态 */ +.hedge-plan-page-wrap { + font-size: 0.8rem; +} +.hedge-plan-page-wrap .card { + padding: 12px 14px; +} +.hedge-plan-page-wrap .card h2, +.hedge-plan-page-wrap .hp-title { + font-size: 0.9rem; + margin: 0 0 8px; + font-weight: 600; +} .hedge-plan-page-wrap .hp-head-card { margin-bottom: 12px; } @@ -3178,20 +3190,55 @@ html[data-theme="light"] .opt-be-dist-down { gap: 10px; align-items: center; justify-content: space-between; - margin-bottom: 12px; + margin-bottom: 10px; } .hedge-plan-page-wrap .hp-title { margin: 0; } .hedge-plan-page-wrap .hp-title-sub { - font-size: 0.85rem; + font-size: 0.75rem; font-weight: 400; } +.hedge-plan-page-wrap .hp-quote-line, +.hedge-plan-page-wrap .hp-acct-hint, +.hedge-plan-page-wrap #hp-sizing-line, +.hedge-plan-page-wrap #hp-gate-line { + font-size: 0.72rem; + line-height: 1.45; + margin: 4px 0 6px; +} +.hedge-plan-page-wrap .hp-acct-tag { + font-size: 0.68rem; + font-weight: 500; + margin-left: 4px; +} +.hedge-plan-page-wrap .form-row label, +.hedge-plan-page-wrap .form-row select, +.hedge-plan-page-wrap .form-row input, +.hedge-plan-page-wrap .form-row .btn-secondary, +.hedge-plan-page-wrap .form-row .primary { + font-size: 0.74rem; +} +.hedge-plan-page-wrap .form-row input[type="number"] { + max-width: 110px; + padding: 4px 6px; + min-height: 28px; +} +.hedge-plan-page-wrap .options-strike-table { + font-size: 0.74rem; +} +.hedge-plan-page-wrap .options-strike-table th, +.hedge-plan-page-wrap .options-strike-table td { + padding: 5px 4px; +} +.hedge-plan-page-wrap .options-strike-table code { + font-size: 0.66rem; +} .hedge-plan-page-wrap .hp-tabs { display: flex; flex-wrap: wrap; gap: 8px; - margin: 0 0 10px; + margin: 0 0 8px; padding: 6px; border-radius: 10px; background: rgba(0, 0, 0, 0.28); @@ -3202,9 +3249,9 @@ html[data-theme="light"] .opt-be-dist-down { border: 1px solid rgba(255, 255, 255, 0.14); background: rgba(255, 255, 255, 0.04); color: #aeb6c5; - font-size: 0.92rem; + font-size: 0.82rem; font-weight: 600; - padding: 9px 16px; + padding: 7px 14px; border-radius: 8px; cursor: pointer; line-height: 1.2; @@ -3221,6 +3268,31 @@ html[data-theme="light"] .opt-be-dist-down { border-color: #fff; box-shadow: 0 0 0 2px rgba(142, 182, 255, 0.55), 0 6px 16px rgba(0, 0, 0, 0.35); } +.hedge-plan-page-wrap .hp-uly-btn, +.hedge-plan-page-wrap .hp-uly-btn-oo, +.hedge-plan-page-wrap .hp-money-btn { + min-width: 52px; + font-weight: 600; + border: 1px solid rgba(255, 255, 255, 0.14); + background: rgba(255, 255, 255, 0.04); + color: #9aa4b2; +} +.hedge-plan-page-wrap .hp-uly-btn.active, +.hedge-plan-page-wrap .hp-uly-btn-oo.active, +.hedge-plan-page-wrap .hp-money-btn.active { + color: #0b1220; + background: linear-gradient(180deg, #ffffff 0%, #9ec0ff 100%); + border-color: #fff; + box-shadow: 0 0 0 2px rgba(100, 160, 255, 0.5); +} +.hedge-plan-page-wrap .hp-money-hint { + font-size: 0.68rem; + margin-left: 4px; +} +.hedge-plan-page-wrap .hp-pick.active, +.hedge-plan-page-wrap .opt-row-selected td { + background: rgba(90, 140, 255, 0.18); +} .hedge-plan-page-wrap .hp-tab-panel.hidden, .hedge-plan-page-wrap .hp-tab-panel[hidden] { display: none !important; @@ -3233,9 +3305,7 @@ html[data-theme="light"] .opt-be-dist-down { border: 1px dashed rgba(255, 255, 255, 0.16); color: #9aa4b2; background: rgba(255, 255, 255, 0.03); -} -.hedge-plan-page-wrap #hp-gate-line { - margin: 0; + font-size: 0.78rem; } html[data-theme="light"] .hedge-plan-page-wrap .hp-tabs { background: rgba(15, 23, 42, 0.06); @@ -3252,6 +3322,13 @@ html[data-theme="light"] .hedge-plan-page-wrap .hp-tab.active { border-color: #2f6fed; box-shadow: 0 0 0 2px rgba(47, 111, 237, 0.25); } +html[data-theme="light"] .hedge-plan-page-wrap .hp-uly-btn.active, +html[data-theme="light"] .hedge-plan-page-wrap .hp-uly-btn-oo.active, +html[data-theme="light"] .hedge-plan-page-wrap .hp-money-btn.active { + color: #0b1220; + background: linear-gradient(180deg, #ffffff 0%, #b9d2ff 100%); + border-color: #2f6fed; +} html[data-theme="light"] .hedge-plan-page-wrap .hp-placeholder { border-color: rgba(15, 23, 42, 0.18); background: rgba(15, 23, 42, 0.03); diff --git a/lib/hedge_plan/hedge_plan_register.py b/lib/hedge_plan/hedge_plan_register.py index 9fe91b9..548e3c6 100644 --- a/lib/hedge_plan/hedge_plan_register.py +++ b/lib/hedge_plan/hedge_plan_register.py @@ -49,7 +49,7 @@ def install_hedge_plan(app: Flask, repo_root: str, app_module: Any) -> None: def _build_cfg(app_module: Any) -> dict[str, Any]: - from lib.exchange.okx_options_lib import build_option_chain + from lib.exchange.okx_options_lib import build_option_chain, options_header_balances return { "get_db": app_module.get_db, @@ -62,6 +62,7 @@ def _build_cfg(app_module: Any) -> dict[str, Any]: "normalize_exchange_symbol": getattr(app_module, "normalize_exchange_symbol", None), "ensure_markets_loaded": getattr(app_module, "ensure_markets_loaded", None), "build_option_chain": build_option_chain, + "options_header_balances": options_header_balances, "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), @@ -69,6 +70,8 @@ def _build_cfg(app_module: Any) -> dict[str, Any]: "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(), } @@ -132,6 +135,9 @@ def register_hedge_plan_routes(app: Flask, cfg: dict[str, Any]) -> None: **data, "gates": gates, "sizing_mode": sizing_mode, + "account_kind": "perp", + "account_label": cfg.get("perp_account_label") or "合约账户", + "account_note": "永续腿使用合约(交易)账户可用 USDT", } return jsonify(out) @@ -154,7 +160,19 @@ def register_hedge_plan_routes(app: Flask, cfg: dict[str, Any]) -> None: ) except Exception as e: return jsonify({"ok": False, "msg": f"拉取期权链失败: {e}"}), 500 - return jsonify({"ok": True, **chain, "chain_max_dte_days": cfg.get("chain_max_dte")}) + 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 @@ -318,6 +336,34 @@ def _fetch_perp_market(cfg: dict[str, Any], base: str) -> tuple[dict[str, Any], }, 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 diff --git a/lib/hedge_plan/templates/hedge_plan_panel.html b/lib/hedge_plan/templates/hedge_plan_panel.html index 3fb70f9..46a9c86 100644 --- a/lib/hedge_plan/templates/hedge_plan_panel.html +++ b/lib/hedge_plan/templates/hedge_plan_panel.html @@ -23,13 +23,14 @@

+

永续腿→合约账户 · 期权腿→期权账户

-

永续(列表行情)

-
+

永续 · ETH 合约账户

+
-
加载中…
+
加载中…
@@ -47,20 +48,27 @@

-

期权(列表) · Put

+

期权(列表) · Put 期权账户

-
+
+ + + + 实值含平值 +
+
+
- + - + @@ -108,13 +116,14 @@
行权价类型实虚值 卖一/张买一买一/张 操作
- + - + - + - + - +
CallCall 行权PutPut
卖一选用卖一/张实虚值选用 K卖一选用实虚值卖一/张选用
请刷新期权链
请刷新期权链
@@ -186,4 +195,4 @@
- + diff --git a/lib/instance/templates/embed_shell.html b/lib/instance/templates/embed_shell.html index 835ed93..0b8b9c4 100644 --- a/lib/instance/templates/embed_shell.html +++ b/lib/instance/templates/embed_shell.html @@ -7,7 +7,7 @@ - + {{ exchange_display }} · 加密货币 | 交易监控复盘系统 diff --git a/lib/instance/templates/index.html b/lib/instance/templates/index.html index 49971e7..06abbe0 100644 --- a/lib/instance/templates/index.html +++ b/lib/instance/templates/index.html @@ -17,7 +17,7 @@ {{ exchange_display }} · 加密货币 | 交易监控复盘系统 - +