Hide zero USDT in options balance labels and add swap/transfer all buttons.
Skip 0 balances in header display; fill max funding/trading amount on 全部兑换/全部划转. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -16,6 +16,43 @@
|
|||||||
el.classList.toggle("opt-error", !!isErr);
|
el.classList.toggle("opt-error", !!isErr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function floorAmount(v) {
|
||||||
|
const n = Number(v);
|
||||||
|
if (!Number.isFinite(n) || n <= 0) return null;
|
||||||
|
return Math.floor(n * 100) / 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadBalances(force) {
|
||||||
|
const q = force ? "?force=1" : "";
|
||||||
|
const d = await apiJson("/api/options/balances" + q);
|
||||||
|
if (!d.ok) throw new Error(d.msg || "余额拉取失败");
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickBalance(bal, account, ccy) {
|
||||||
|
const acct = account === "trading" ? "trading" : "funding";
|
||||||
|
const key = acct + "_" + String(ccy || "").toLowerCase();
|
||||||
|
return floorAmount(bal[key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveMaxAmount(account, ccy) {
|
||||||
|
const bal = await loadBalances(true);
|
||||||
|
return pickBalance(bal, account, ccy);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitSwap(amount) {
|
||||||
|
const d = await apiJson("/api/options/spot/swap", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
direction: document.getElementById("opt-set-swap-dir").value,
|
||||||
|
amount: amount,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
setMsg("opt-set-swap-msg", d.ok ? "兑换单已提交" : (d.msg || "失败"), !d.ok);
|
||||||
|
if (d.ok && typeof refreshAccountSnapshot === "function") refreshAccountSnapshot();
|
||||||
|
}
|
||||||
|
|
||||||
const swapBtn = document.getElementById("opt-set-swap-btn");
|
const swapBtn = document.getElementById("opt-set-swap-btn");
|
||||||
if (swapBtn) {
|
if (swapBtn) {
|
||||||
swapBtn.addEventListener("click", async function () {
|
swapBtn.addEventListener("click", async function () {
|
||||||
@@ -24,18 +61,44 @@
|
|||||||
setMsg("opt-set-swap-msg", "请输入有效数量", true);
|
setMsg("opt-set-swap-msg", "请输入有效数量", true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const d = await apiJson("/api/options/spot/swap", {
|
await submitSwap(amount);
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({
|
|
||||||
direction: document.getElementById("opt-set-swap-dir").value,
|
|
||||||
amount: amount,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
setMsg("opt-set-swap-msg", d.ok ? "兑换单已提交" : (d.msg || "失败"), !d.ok);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const swapAllBtn = document.getElementById("opt-set-swap-all-btn");
|
||||||
|
if (swapAllBtn) {
|
||||||
|
swapAllBtn.addEventListener("click", async function () {
|
||||||
|
try {
|
||||||
|
const dir = document.getElementById("opt-set-swap-dir").value;
|
||||||
|
const ccy = dir === "usdc_to_usdt" ? "USDC" : "USDT";
|
||||||
|
const amount = await resolveMaxAmount("funding", ccy);
|
||||||
|
if (!amount) {
|
||||||
|
setMsg("opt-set-swap-msg", "资金账户可用余额不足", true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
document.getElementById("opt-set-swap-amount").value = String(amount);
|
||||||
|
await submitSwap(amount);
|
||||||
|
} catch (e) {
|
||||||
|
setMsg("opt-set-swap-msg", e.message || "余额拉取失败", true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitInternalTransfer(amount) {
|
||||||
|
const d = await apiJson("/api/options/transfer", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
ccy: document.getElementById("opt-set-int-ccy").value,
|
||||||
|
from: document.getElementById("opt-set-int-from").value,
|
||||||
|
to: document.getElementById("opt-set-int-to").value,
|
||||||
|
amount: amount,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
setMsg("opt-set-int-msg", d.ok ? "划转成功" : (d.msg || "失败"), !d.ok);
|
||||||
|
if (d.ok && typeof refreshAccountSnapshot === "function") refreshAccountSnapshot();
|
||||||
|
}
|
||||||
|
|
||||||
const intBtn = document.getElementById("opt-set-int-btn");
|
const intBtn = document.getElementById("opt-set-int-btn");
|
||||||
if (intBtn) {
|
if (intBtn) {
|
||||||
intBtn.addEventListener("click", async function () {
|
intBtn.addEventListener("click", async function () {
|
||||||
@@ -44,20 +107,45 @@
|
|||||||
setMsg("opt-set-int-msg", "请输入有效数量", true);
|
setMsg("opt-set-int-msg", "请输入有效数量", true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const d = await apiJson("/api/options/transfer", {
|
await submitInternalTransfer(amount);
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({
|
|
||||||
ccy: document.getElementById("opt-set-int-ccy").value,
|
|
||||||
from: document.getElementById("opt-set-int-from").value,
|
|
||||||
to: document.getElementById("opt-set-int-to").value,
|
|
||||||
amount: amount,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
setMsg("opt-set-int-msg", d.ok ? "划转成功" : (d.msg || "失败"), !d.ok);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const intAllBtn = document.getElementById("opt-set-int-all-btn");
|
||||||
|
if (intAllBtn) {
|
||||||
|
intAllBtn.addEventListener("click", async function () {
|
||||||
|
try {
|
||||||
|
const ccy = document.getElementById("opt-set-int-ccy").value;
|
||||||
|
const from = document.getElementById("opt-set-int-from").value;
|
||||||
|
const amount = await resolveMaxAmount(from, ccy);
|
||||||
|
if (!amount) {
|
||||||
|
setMsg("opt-set-int-msg", "划出账户可用余额不足", true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
document.getElementById("opt-set-int-amount").value = String(amount);
|
||||||
|
await submitInternalTransfer(amount);
|
||||||
|
} catch (e) {
|
||||||
|
setMsg("opt-set-int-msg", e.message || "余额拉取失败", true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitCrossTransfer(amount) {
|
||||||
|
const d = await apiJson("/api/options/cross-transfer", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
ccy: document.getElementById("opt-set-cross-ccy").value,
|
||||||
|
amount: amount,
|
||||||
|
from_account: document.getElementById("opt-set-cross-from").value,
|
||||||
|
to_account: document.getElementById("opt-set-cross-to").value,
|
||||||
|
direction: document.getElementById("opt-set-cross-dir").value,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
setMsg("opt-set-cross-msg", d.ok ? "划转成功" : (d.msg || "失败"), !d.ok);
|
||||||
|
if (d.ok && typeof refreshAccountSnapshot === "function") refreshAccountSnapshot();
|
||||||
|
}
|
||||||
|
|
||||||
const crossBtn = document.getElementById("opt-set-cross-btn");
|
const crossBtn = document.getElementById("opt-set-cross-btn");
|
||||||
if (crossBtn) {
|
if (crossBtn) {
|
||||||
crossBtn.addEventListener("click", async function () {
|
crossBtn.addEventListener("click", async function () {
|
||||||
@@ -66,19 +154,26 @@
|
|||||||
setMsg("opt-set-cross-msg", "请输入有效数量", true);
|
setMsg("opt-set-cross-msg", "请输入有效数量", true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const d = await apiJson("/api/options/cross-transfer", {
|
await submitCrossTransfer(amount);
|
||||||
method: "POST",
|
});
|
||||||
headers: { "Content-Type": "application/json" },
|
}
|
||||||
body: JSON.stringify({
|
|
||||||
ccy: document.getElementById("opt-set-cross-ccy").value,
|
const crossAllBtn = document.getElementById("opt-set-cross-all-btn");
|
||||||
amount: amount,
|
if (crossAllBtn) {
|
||||||
from_account: document.getElementById("opt-set-cross-from").value,
|
crossAllBtn.addEventListener("click", async function () {
|
||||||
to_account: document.getElementById("opt-set-cross-to").value,
|
try {
|
||||||
direction: document.getElementById("opt-set-cross-dir").value,
|
const ccy = document.getElementById("opt-set-cross-ccy").value;
|
||||||
}),
|
const from = document.getElementById("opt-set-cross-from").value;
|
||||||
});
|
const amount = await resolveMaxAmount(from, ccy);
|
||||||
setMsg("opt-set-cross-msg", d.ok ? "划转成功" : (d.msg || "失败"), !d.ok);
|
if (!amount) {
|
||||||
if (d.ok && typeof refreshAccountSnapshot === "function") refreshAccountSnapshot();
|
setMsg("opt-set-cross-msg", "划出账户可用余额不足", true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
document.getElementById("opt-set-cross-amount").value = String(amount);
|
||||||
|
await submitCrossTransfer(amount);
|
||||||
|
} catch (e) {
|
||||||
|
setMsg("opt-set-cross-msg", e.message || "余额拉取失败", true);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -90,9 +90,9 @@ def options_funding_label(
|
|||||||
funding_usdt: float | None = None,
|
funding_usdt: float | None = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
parts: list[str] = []
|
parts: list[str] = []
|
||||||
if funding_usdc is not None:
|
if funding_usdc is not None and float(funding_usdc) > 0:
|
||||||
parts.append(f"{float(funding_usdc):.2f} USDC")
|
parts.append(f"{float(funding_usdc):.2f} USDC")
|
||||||
if funding_usdt is not None:
|
if funding_usdt is not None and float(funding_usdt) > 0:
|
||||||
parts.append(f"{float(funding_usdt):.2f} USDT")
|
parts.append(f"{float(funding_usdt):.2f} USDT")
|
||||||
return " · ".join(parts) if parts else "—"
|
return " · ".join(parts) if parts else "—"
|
||||||
|
|
||||||
|
|||||||
@@ -1134,8 +1134,8 @@ function paintRealtimePnlFromSnapshot(data){
|
|||||||
|
|
||||||
function formatOptionsFundingLabel(usdc, usdt) {
|
function formatOptionsFundingLabel(usdc, usdt) {
|
||||||
const parts = [];
|
const parts = [];
|
||||||
if (usdc !== null && usdc !== undefined) parts.push(`${Number(usdc).toFixed(2)} USDC`);
|
if (usdc !== null && usdc !== undefined && Number(usdc) > 0) parts.push(`${Number(usdc).toFixed(2)} USDC`);
|
||||||
if (usdt !== null && usdt !== undefined) parts.push(`${Number(usdt).toFixed(2)} USDT`);
|
if (usdt !== null && usdt !== undefined && Number(usdt) > 0) parts.push(`${Number(usdt).toFixed(2)} USDT`);
|
||||||
return parts.length ? parts.join(" · ") : "—";
|
return parts.length ? parts.join(" · ") : "—";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1779,8 +1779,8 @@ function paintRealtimePnlFromSnapshot(data){
|
|||||||
|
|
||||||
function formatOptionsFundingLabel(usdc, usdt) {
|
function formatOptionsFundingLabel(usdc, usdt) {
|
||||||
const parts = [];
|
const parts = [];
|
||||||
if (usdc !== null && usdc !== undefined) parts.push(`${Number(usdc).toFixed(2)} USDC`);
|
if (usdc !== null && usdc !== undefined && Number(usdc) > 0) parts.push(`${Number(usdc).toFixed(2)} USDC`);
|
||||||
if (usdt !== null && usdt !== undefined) parts.push(`${Number(usdt).toFixed(2)} USDT`);
|
if (usdt !== null && usdt !== undefined && Number(usdt) > 0) parts.push(`${Number(usdt).toFixed(2)} USDT`);
|
||||||
return parts.length ? parts.join(" · ") : "—";
|
return parts.length ? parts.join(" · ") : "—";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -280,7 +280,8 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
|||||||
ex, err = _require_options_ex(cfg)
|
ex, err = _require_options_ex(cfg)
|
||||||
if ex is None:
|
if ex is None:
|
||||||
return jsonify({"ok": False, "msg": err})
|
return jsonify({"ok": False, "msg": err})
|
||||||
bal = cfg["fetch_options_balances"](ex)
|
force = (request.args.get("force") or "").strip().lower() in ("1", "true", "yes")
|
||||||
|
bal = cfg["fetch_options_balances"](ex, force=force)
|
||||||
return jsonify({"ok": True, **bal, "trade_budget": cfg["trade_budget"]})
|
return jsonify({"ok": True, **bal, "trade_budget": cfg["trade_budget"]})
|
||||||
|
|
||||||
@app.route("/api/options/chain")
|
@app.route("/api/options/chain")
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{# 期权设置脚本挂载点(卡片在 settings_panel 中拆分) #}
|
{# 期权设置脚本挂载点(卡片在 settings_panel 中拆分) #}
|
||||||
<div id="options-settings-root" hidden
|
<div id="options-settings-root" hidden
|
||||||
data-sub-account="{{ instance_settings.options_sub_account | default('', true) }}"></div>
|
data-sub-account="{{ instance_settings.options_sub_account | default('', true) }}"></div>
|
||||||
<script src="/static/options_settings.js?v=5"></script>
|
<script src="/static/options_settings.js?v=6"></script>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
<option value="usdc_to_usdt">USDC → USDT</option>
|
<option value="usdc_to_usdt">USDC → USDT</option>
|
||||||
</select>
|
</select>
|
||||||
<input type="number" id="opt-set-swap-amount" min="0.01" step="0.01" placeholder="数量">
|
<input type="number" id="opt-set-swap-amount" min="0.01" step="0.01" placeholder="数量">
|
||||||
|
<button type="button" class="btn-secondary btn-sm" id="opt-set-swap-all-btn">全部兑换</button>
|
||||||
<button type="button" class="btn-primary btn-sm" id="opt-set-swap-btn">市价兑换</button>
|
<button type="button" class="btn-primary btn-sm" id="opt-set-swap-btn">市价兑换</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="opt-set-swap-msg" class="options-settings-msg muted"></div>
|
<div id="opt-set-swap-msg" class="options-settings-msg muted"></div>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
<option value="funding">to: 资金</option>
|
<option value="funding">to: 资金</option>
|
||||||
</select>
|
</select>
|
||||||
<input type="number" id="opt-set-int-amount" min="0.01" step="0.01" placeholder="数量">
|
<input type="number" id="opt-set-int-amount" min="0.01" step="0.01" placeholder="数量">
|
||||||
|
<button type="button" class="btn-secondary btn-sm" id="opt-set-int-all-btn">全部划转</button>
|
||||||
<button type="button" class="btn-primary btn-sm" id="opt-set-int-btn">划转</button>
|
<button type="button" class="btn-primary btn-sm" id="opt-set-int-btn">划转</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="opt-set-int-msg" class="options-settings-msg muted"></div>
|
<div id="opt-set-int-msg" class="options-settings-msg muted"></div>
|
||||||
@@ -42,6 +43,7 @@
|
|||||||
<option value="funding">to: 资金</option>
|
<option value="funding">to: 资金</option>
|
||||||
</select>
|
</select>
|
||||||
<input type="number" id="opt-set-cross-amount" min="0.01" step="0.01" placeholder="数量">
|
<input type="number" id="opt-set-cross-amount" min="0.01" step="0.01" placeholder="数量">
|
||||||
|
<button type="button" class="btn-secondary btn-sm" id="opt-set-cross-all-btn">全部划转</button>
|
||||||
<button type="button" class="btn-primary btn-sm" id="opt-set-cross-btn">划转</button>
|
<button type="button" class="btn-primary btn-sm" id="opt-set-cross-btn">划转</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="opt-set-cross-msg" class="options-settings-msg muted"></div>
|
<div id="opt-set-cross-msg" class="options-settings-msg muted"></div>
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ class TestHeaderStatsLib(unittest.TestCase):
|
|||||||
|
|
||||||
def test_options_funding_label(self):
|
def test_options_funding_label(self):
|
||||||
self.assertEqual(options_funding_label(1.5, 10), "1.50 USDC · 10.00 USDT")
|
self.assertEqual(options_funding_label(1.5, 10), "1.50 USDC · 10.00 USDT")
|
||||||
|
self.assertEqual(options_funding_label(10.19, 0), "10.19 USDC")
|
||||||
self.assertEqual(options_funding_label(None, 10), "10.00 USDT")
|
self.assertEqual(options_funding_label(None, 10), "10.00 USDT")
|
||||||
self.assertEqual(options_funding_label(None, None), "—")
|
self.assertEqual(options_funding_label(None, None), "—")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user