期权页增加重试卖回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 {
|
.options-pos-head h2 {
|
||||||
margin: 0;
|
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 {
|
.opt-pos-card {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -268,6 +268,7 @@
|
|||||||
await refreshPendingOrders();
|
await refreshPendingOrders();
|
||||||
refreshAllPositions();
|
refreshAllPositions();
|
||||||
if (typeof refreshAccountSnapshot === "function") refreshAccountSnapshot();
|
if (typeof refreshAccountSnapshot === "function") refreshAccountSnapshot();
|
||||||
|
void refreshBridgeSellUi(true);
|
||||||
} finally {
|
} finally {
|
||||||
if (btn) btn.disabled = false;
|
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() {
|
function updateSizeInputs() {
|
||||||
const mode = currentSizeMode();
|
const mode = currentSizeMode();
|
||||||
const sheetsEl = document.getElementById("opt-sheets-amount");
|
const sheetsEl = document.getElementById("opt-sheets-amount");
|
||||||
@@ -1508,6 +1619,7 @@
|
|||||||
parkOrderPanel();
|
parkOrderPanel();
|
||||||
}
|
}
|
||||||
updateUnderlyingLabel();
|
updateUnderlyingLabel();
|
||||||
|
updateRetrySellCoinUi(lastBalancesPayload);
|
||||||
renderExpiries();
|
renderExpiries();
|
||||||
if (soft && keepExp) {
|
if (soft && keepExp) {
|
||||||
const sel = document.getElementById("opt-exp-select");
|
const sel = document.getElementById("opt-exp-select");
|
||||||
@@ -2195,12 +2307,20 @@
|
|||||||
}
|
}
|
||||||
if (r.remaining_sheets > 0) okMsg += "\n剩余: " + r.remaining_sheets + " 张(下次再平)";
|
if (r.remaining_sheets > 0) okMsg += "\n剩余: " + r.remaining_sheets + " 张(下次再平)";
|
||||||
if (r.stopped_reason) okMsg += "\n状态: " + r.stopped_reason;
|
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);
|
alert(okMsg);
|
||||||
} else {
|
} else {
|
||||||
alert(r.msg || "平仓失败");
|
alert(r.msg || "平仓失败");
|
||||||
}
|
}
|
||||||
refreshAllPositions();
|
refreshAllPositions();
|
||||||
if (typeof refreshAccountSnapshot === "function") refreshAccountSnapshot();
|
if (typeof refreshAccountSnapshot === "function") refreshAccountSnapshot();
|
||||||
|
void refreshBridgeSellUi(true);
|
||||||
} finally {
|
} finally {
|
||||||
if (btn) btn.disabled = false;
|
if (btn) btn.disabled = false;
|
||||||
}
|
}
|
||||||
@@ -2644,13 +2764,7 @@
|
|||||||
applyBudgetBuffer(state.budgetBuffer);
|
applyBudgetBuffer(state.budgetBuffer);
|
||||||
updateSizeInputs();
|
updateSizeInputs();
|
||||||
void (async function syncLiveCompoundFlag() {
|
void (async function syncLiveCompoundFlag() {
|
||||||
try {
|
await refreshBridgeSellUi(false);
|
||||||
const d = await apiJson("/api/options/balances");
|
|
||||||
syncCompoundFlagsFromPayload(d);
|
|
||||||
if (d && d.trade_budget != null && root) {
|
|
||||||
root.dataset.tradeBudget = String(d.trade_budget);
|
|
||||||
}
|
|
||||||
} catch (_) {}
|
|
||||||
})();
|
})();
|
||||||
syncMoneyFilterButtons();
|
syncMoneyFilterButtons();
|
||||||
syncChainViewUI();
|
syncChainViewUI();
|
||||||
@@ -2681,6 +2795,7 @@
|
|||||||
document.querySelectorAll(".opt-uly-btn").forEach(function (b) { b.classList.remove("active"); });
|
document.querySelectorAll(".opt-uly-btn").forEach(function (b) { b.classList.remove("active"); });
|
||||||
btn.classList.add("active");
|
btn.classList.add("active");
|
||||||
state.underlying = btn.getAttribute("data-uly");
|
state.underlying = btn.getAttribute("data-uly");
|
||||||
|
updateRetrySellCoinUi(lastBalancesPayload);
|
||||||
loadChain();
|
loadChain();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -2735,6 +2850,8 @@
|
|||||||
document.getElementById("opt-exp-select").addEventListener("change", onExpiryChange);
|
document.getElementById("opt-exp-select").addEventListener("change", onExpiryChange);
|
||||||
document.getElementById("opt-load-chain").addEventListener("click", loadChain);
|
document.getElementById("opt-load-chain").addEventListener("click", loadChain);
|
||||||
document.getElementById("opt-refresh-positions").addEventListener("click", refreshAllPositions);
|
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);
|
document.getElementById("opt-open-btn").addEventListener("click", openPosition);
|
||||||
const pendingRefreshBtn = document.getElementById("opt-pending-refresh");
|
const pendingRefreshBtn = document.getElementById("opt-pending-refresh");
|
||||||
if (pendingRefreshBtn) {
|
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)
|
payload["coin_budget"] = coin_budget_preview(cfg, ex)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
payload["coin_budget"] = {"ok": False, "msg": str(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)
|
return jsonify(payload)
|
||||||
|
|
||||||
@app.route("/api/options/chain")
|
@app.route("/api/options/chain")
|
||||||
|
|||||||
@@ -185,7 +185,11 @@
|
|||||||
<div class="card options-pos-card-wrap">
|
<div class="card options-pos-card-wrap">
|
||||||
<div class="options-pos-head">
|
<div class="options-pos-head">
|
||||||
<h2>持仓</h2>
|
<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>
|
||||||
<div class="options-pos-tabs" role="tablist" aria-label="持仓面板">
|
<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>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
<script src="/static/options_expiry_countdown.js?v=1"></script>
|
<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