期权页增加重试卖回ETH按钮,平仓后可手动全额卖回交易户标的币
币本位模式下持仓区显示重试按钮与可用余额提示,调用已有 spot-bridge 接口按交易账户全部可用量市价卖回 USDT。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -185,7 +185,11 @@
|
||||
<div class="card options-pos-card-wrap">
|
||||
<div class="options-pos-head">
|
||||
<h2>持仓</h2>
|
||||
<button type="button" class="btn-secondary" id="opt-refresh-positions">刷新</button>
|
||||
<div class="options-pos-head-actions">
|
||||
<span id="opt-bridge-sell-hint" class="muted opt-bridge-sell-hint" hidden></span>
|
||||
<button type="button" class="btn-secondary opt-retry-sell-coin-btn" id="opt-retry-sell-coin" hidden title="市价卖出交易账户全部可用标的币换回 USDT">重试卖回</button>
|
||||
<button type="button" class="btn-secondary" id="opt-refresh-positions">刷新</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="options-pos-tabs" role="tablist" aria-label="持仓面板">
|
||||
<button type="button" class="btn-secondary opt-pos-tab active" data-opt-pos-tab="live" role="tab" aria-selected="true" id="opt-pos-tab-live">当前持仓</button>
|
||||
@@ -351,4 +355,4 @@
|
||||
</div>
|
||||
</div>
|
||||
<script src="/static/options_expiry_countdown.js?v=1"></script>
|
||||
<script src="/static/options_panel.js?v=66"></script>
|
||||
<script src="/static/options_panel.js?v=67"></script>
|
||||
|
||||
Reference in New Issue
Block a user