From fe5bb923d7eb7bb90de2b73ec6b570a1b4f35cb7 Mon Sep 17 00:00:00 2001 From: dekun Date: Sun, 23 Aug 2026 08:01:19 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9C=9F=E6=9D=83=E9=A1=B5=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E9=87=8D=E8=AF=95=E5=8D=96=E5=9B=9EETH=E6=8C=89=E9=92=AE?= =?UTF-8?q?=EF=BC=8C=E5=B9=B3=E4=BB=93=E5=90=8E=E5=8F=AF=E6=89=8B=E5=8A=A8?= =?UTF-8?q?=E5=85=A8=E9=A2=9D=E5=8D=96=E5=9B=9E=E4=BA=A4=E6=98=93=E6=88=B7?= =?UTF-8?q?=E6=A0=87=E7=9A=84=E5=B8=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 币本位模式下持仓区显示重试按钮与可用余额提示,调用已有 spot-bridge 接口按交易账户全部可用量市价卖回 USDT。 Co-authored-by: Cursor --- lib/common/static/instance_theme.css | 18 ++++ lib/common/static/options_panel.js | 131 +++++++++++++++++++++-- lib/options/options_register.py | 13 +++ lib/options/templates/options_panel.html | 8 +- 4 files changed, 161 insertions(+), 9 deletions(-) diff --git a/lib/common/static/instance_theme.css b/lib/common/static/instance_theme.css index 638fb66..f3e5f64 100644 --- a/lib/common/static/instance_theme.css +++ b/lib/common/static/instance_theme.css @@ -5279,6 +5279,24 @@ html[data-theme="light"] .opt-source-badge--oo { .options-pos-head h2 { margin: 0; } +.options-pos-head-actions { + display: flex; + align-items: center; + justify-content: flex-end; + flex-wrap: wrap; + gap: 8px; + margin-left: auto; +} +.opt-bridge-sell-hint { + font-size: 0.78rem; + line-height: 1.35; + max-width: 280px; + text-align: right; +} +.opt-retry-sell-coin-btn--pending { + border-color: #ffb347; + color: #ffb347; +} .opt-pos-card { margin-bottom: 10px; } diff --git a/lib/common/static/options_panel.js b/lib/common/static/options_panel.js index bbf206d..a2d12b4 100644 --- a/lib/common/static/options_panel.js +++ b/lib/common/static/options_panel.js @@ -268,6 +268,7 @@ await refreshPendingOrders(); refreshAllPositions(); if (typeof refreshAccountSnapshot === "function") refreshAccountSnapshot(); + void refreshBridgeSellUi(true); } finally { if (btn) btn.disabled = false; } @@ -301,6 +302,116 @@ } } + let lastBalancesPayload = null; + + function tradingCoinAvailFromPayload(d, uly) { + if (!d) return null; + const u = String(uly || state.underlying || "ETH").toUpperCase(); + const key = u === "BTC" ? "trading_btc_avail" : "trading_eth_avail"; + const v = d[key]; + if (v == null) return d[u === "BTC" ? "trading_btc" : "trading_eth"]; + return v; + } + + function coinMarginModeFromPayload(d) { + if (d && d.options_margin_mode === "coin") return true; + if (d && d.options_margin_mode === "usdc") return false; + return isCoinMarginMode(); + } + + function updateRetrySellCoinUi(d) { + const btn = document.getElementById("opt-retry-sell-coin"); + const hint = document.getElementById("opt-bridge-sell-hint"); + if (!btn) return; + const payload = d || lastBalancesPayload; + const coinOn = coinMarginModeFromPayload(payload); + const uly = String(state.underlying || "ETH").toUpperCase(); + btn.hidden = !coinOn; + if (!coinOn) { + if (hint) hint.hidden = true; + btn.classList.remove("opt-retry-sell-coin-btn--pending"); + return; + } + btn.textContent = "重试卖回 " + uly; + const avail = tradingCoinAvailFromPayload(payload, uly); + const pending = payload && payload.bridge_status === "pending_sell_spot"; + btn.classList.toggle("opt-retry-sell-coin-btn--pending", !!pending); + if (hint) { + const parts = []; + if (pending) parts.push("平仓后卖币未成功,可点此重试"); + if (avail != null && Number(avail) > 0) { + parts.push("交易户可用 " + fmt(Number(avail), 6) + " " + uly); + } + if (parts.length) { + hint.textContent = parts.join(" · "); + hint.hidden = false; + } else { + hint.hidden = true; + } + } + } + + async function refreshBridgeSellUi(force) { + try { + const qs = force ? "?force=1" : ""; + const d = await apiJson("/api/options/balances" + qs); + lastBalancesPayload = d; + syncCompoundFlagsFromPayload(d); + if (d && d.options_margin_mode === "coin") { + if (!state.chain) state.chain = {}; + state.chain.options_margin_mode = "coin"; + state.chain.margin_mode = "coin"; + } + if (d && d.trade_budget != null && root) { + root.dataset.tradeBudget = String(d.trade_budget); + } + updateRetrySellCoinUi(d); + return d; + } catch (_) { + updateRetrySellCoinUi(null); + return null; + } + } + + async function retrySellBackCoin() { + const uly = String(state.underlying || "ETH").toUpperCase(); + const d = await refreshBridgeSellUi(true); + const avail = tradingCoinAvailFromPayload(d, uly); + const availTxt = + avail != null && Number(avail) > 0 + ? ("\n交易账户可用: " + fmt(Number(avail), 6) + " " + uly) + : ""; + const msg = + "确认市价卖出交易账户中的全部可用 " + uly + " 换回 USDT?" + + availTxt + + "\n\n将按可用余额全额卖出(非 bridge 记账量)。"; + if (!confirm(msg)) return; + const btn = document.getElementById("opt-retry-sell-coin"); + if (btn) btn.disabled = true; + try { + const r = await apiJson("/api/options/spot-bridge/retry-sell", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ underlying: uly }), + }); + if (r.ok) { + if (r.skipped) { + alert(r.msg || "无残留币需卖回"); + } else { + const sold = r.sell && r.sell.coin_sold; + alert("卖回已提交" + (sold != null ? (" " + fmt(Number(sold), 6) + " " + uly) : "")); + } + } else { + alert(r.msg || "卖回失败"); + } + refreshAllPositions(); + if (typeof refreshAccountSnapshot === "function") refreshAccountSnapshot(); + await refreshBridgeSellUi(true); + } finally { + if (btn) btn.disabled = false; + } + } + function updateSizeInputs() { const mode = currentSizeMode(); const sheetsEl = document.getElementById("opt-sheets-amount"); @@ -1508,6 +1619,7 @@ parkOrderPanel(); } updateUnderlyingLabel(); + updateRetrySellCoinUi(lastBalancesPayload); renderExpiries(); if (soft && keepExp) { const sel = document.getElementById("opt-exp-select"); @@ -2195,12 +2307,20 @@ } if (r.remaining_sheets > 0) okMsg += "\n剩余: " + r.remaining_sheets + " 张(下次再平)"; if (r.stopped_reason) okMsg += "\n状态: " + r.stopped_reason; + const spotSell = r.spot_sell; + if (spotSell && spotSell.ok === false) { + okMsg += "\n卖回 " + premCcy + " 失败: " + (spotSell.msg || "未知错误"); + okMsg += "\n可点「重试卖回 " + premCcy + "」手动卖出交易户全部可用币。"; + } else if (spotSell && spotSell.ok && spotSell.sell && spotSell.sell.coin_sold != null) { + okMsg += "\n已卖回: " + fmt(Number(spotSell.sell.coin_sold), 6) + " " + premCcy; + } alert(okMsg); } else { alert(r.msg || "平仓失败"); } refreshAllPositions(); if (typeof refreshAccountSnapshot === "function") refreshAccountSnapshot(); + void refreshBridgeSellUi(true); } finally { if (btn) btn.disabled = false; } @@ -2644,13 +2764,7 @@ applyBudgetBuffer(state.budgetBuffer); updateSizeInputs(); void (async function syncLiveCompoundFlag() { - try { - const d = await apiJson("/api/options/balances"); - syncCompoundFlagsFromPayload(d); - if (d && d.trade_budget != null && root) { - root.dataset.tradeBudget = String(d.trade_budget); - } - } catch (_) {} + await refreshBridgeSellUi(false); })(); syncMoneyFilterButtons(); syncChainViewUI(); @@ -2681,6 +2795,7 @@ document.querySelectorAll(".opt-uly-btn").forEach(function (b) { b.classList.remove("active"); }); btn.classList.add("active"); state.underlying = btn.getAttribute("data-uly"); + updateRetrySellCoinUi(lastBalancesPayload); loadChain(); }); }); @@ -2735,6 +2850,8 @@ document.getElementById("opt-exp-select").addEventListener("change", onExpiryChange); document.getElementById("opt-load-chain").addEventListener("click", loadChain); document.getElementById("opt-refresh-positions").addEventListener("click", refreshAllPositions); + const retrySellBtn = document.getElementById("opt-retry-sell-coin"); + if (retrySellBtn) retrySellBtn.addEventListener("click", retrySellBackCoin); document.getElementById("opt-open-btn").addEventListener("click", openPosition); const pendingRefreshBtn = document.getElementById("opt-pending-refresh"); if (pendingRefreshBtn) { diff --git a/lib/options/options_register.py b/lib/options/options_register.py index 78ae9f2..9ca4579 100644 --- a/lib/options/options_register.py +++ b/lib/options/options_register.py @@ -442,6 +442,19 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None: payload["coin_budget"] = coin_budget_preview(cfg, ex) except Exception as e: payload["coin_budget"] = {"ok": False, "msg": str(e)} + conn = cfg["get_db"]() + try: + init_options_tables(conn) + from lib.options.options_spot_bridge_lib import list_open_bridges + + open_bridges = list_open_bridges(conn) + if open_bridges: + payload["bridge_status"] = str(open_bridges[0].get("status") or "") + payload["bridge_underlying"] = str(open_bridges[0].get("underlying") or "") + except Exception: + pass + finally: + conn.close() return jsonify(payload) @app.route("/api/options/chain") diff --git a/lib/options/templates/options_panel.html b/lib/options/templates/options_panel.html index f6b3cb2..0b90c44 100644 --- a/lib/options/templates/options_panel.html +++ b/lib/options/templates/options_panel.html @@ -185,7 +185,11 @@

持仓

- +
+ + + +
@@ -351,4 +355,4 @@
- +