' +
@@ -1577,18 +1577,55 @@
function setSellCoinMsg(text, kind) {
const el = $("hp-sell-coin-msg");
if (!el) return;
+ const show = !!(text || "").trim();
+ el.hidden = !show;
el.textContent = text || "";
el.classList.toggle("is-err", kind === "err");
el.classList.toggle("is-ok", kind === "ok");
el.classList.toggle("muted", !kind);
}
+ function setSellCoinVisible(visible, meta) {
+ const btn = $("hp-sell-coin-btn");
+ if (!btn) return;
+ const on = !!visible;
+ btn.hidden = !on;
+ btn.disabled = !on;
+ if (on && meta) {
+ const uly = String(meta.underlying || state.underlying || "ETH").toUpperCase();
+ const avail = meta.available != null ? Number(meta.available) : null;
+ const minSz = meta.min_sz != null ? Number(meta.min_sz) : null;
+ let tip = "市价卖出交易账户全部可用 " + uly + " 换回 USDT";
+ if (avail != null && !Number.isNaN(avail)) tip += " · 可用 " + avail;
+ if (minSz != null && !Number.isNaN(minSz)) tip += " · 最小 " + minSz;
+ btn.title = tip;
+ }
+ if (!on) setSellCoinMsg("", null);
+ }
+
+ async function refreshSellCoinVisibility() {
+ const uly = String(state.underlying || "ETH").toUpperCase();
+ try {
+ const d = await apiJson(
+ "/api/hedge-plan/sell-coin-status?underlying=" + encodeURIComponent(uly)
+ );
+ if (!d || !d.ok) {
+ setSellCoinVisible(false);
+ return;
+ }
+ setSellCoinVisible(!!d.sellable, d);
+ } catch (_) {
+ setSellCoinVisible(false);
+ }
+ }
+
async function submitSellCoin() {
const uly = String(state.underlying || "ETH").toUpperCase();
+ const btn = $("hp-sell-coin-btn");
+ if (!btn || btn.hidden) return;
if (!window.confirm("确认市价卖出交易账户全部可用 " + uly + " 换回 USDT?")) return;
setSellCoinMsg("卖回中…", null);
- const btn = $("hp-sell-coin-btn");
- if (btn) btn.disabled = true;
+ btn.disabled = true;
try {
const d = await apiJson("/api/hedge-plan/sell-coin", {
method: "POST",
@@ -1597,6 +1634,7 @@
});
if (!d.ok) {
setSellCoinMsg(d.msg || "卖回失败", "err");
+ await refreshSellCoinVisibility();
return;
}
const sold =
@@ -1606,10 +1644,10 @@
setSellCoinMsg(sold, "ok");
await loadChain();
void loadLivePositions();
+ await refreshSellCoinVisibility();
} catch (e) {
setSellCoinMsg(e.message || "卖回失败", "err");
- } finally {
- if (btn) btn.disabled = false;
+ await refreshSellCoinVisibility();
}
}
@@ -2059,6 +2097,7 @@
$("hp-profit-rr").value = "2";
}
}
+ void refreshSellCoinVisibility();
}
function currentExp(selectId) {
@@ -2186,14 +2225,6 @@
}
tbody.appendChild(tr);
});
- if (isOptionPrimary() && !state.poStrikeExpandAll && state.moneyFilter === "all") {
- const hintTr = document.createElement("tr");
- hintTr.innerHTML =
- '| 默认显示约 5 档(平值 + 实值2 + 虚值2) · 勾选「展开全部」查看该到期更多行权价 | ';
- tbody.appendChild(hintTr);
- }
tbody.querySelectorAll(".hp-pick").forEach(function (btn) {
btn.addEventListener("click", function () {
const inst = btn.getAttribute("data-inst");
diff --git a/lib/hedge_plan/hedge_plan_register.py b/lib/hedge_plan/hedge_plan_register.py
index bc5aba2..0cf362e 100644
--- a/lib/hedge_plan/hedge_plan_register.py
+++ b/lib/hedge_plan/hedge_plan_register.py
@@ -1281,10 +1281,47 @@ def register_hedge_plan_routes(app: Flask, cfg: dict[str, Any]) -> None:
pass
return jsonify(result)
+ @app.route("/api/hedge-plan/sell-coin-status")
+ @lr
+ def api_hedge_sell_coin_status():
+ """是否显示「手动卖回币」:交易户有币且 ≥ 现货最小卖出量(粉尘隐藏)."""
+ if not _hedge_enabled():
+ return jsonify({"ok": False, "sellable": False, "msg": "对冲模块未启用"})
+ ex = cfg.get("exchange_options") or cfg.get("exchange")
+ if ex is None:
+ return jsonify({"ok": False, "sellable": False, "msg": "交易所未配置"})
+ underlying = str(
+ request.args.get("underlying") or cfg.get("default_underly") or "ETH"
+ ).strip().upper()
+ if underlying not in ("ETH", "BTC"):
+ return jsonify({"ok": False, "sellable": False, "msg": "标的仅支持 ETH/BTC"})
+ from lib.options.options_spot_bridge_lib import (
+ fetch_trading_coin_available,
+ is_sellable_spot_coin,
+ spot_min_sell_sz,
+ )
+
+ avail = fetch_trading_coin_available(ex, underlying)
+ try:
+ avail_f = float(avail) if avail is not None else 0.0
+ except (TypeError, ValueError):
+ avail_f = 0.0
+ min_sz = float(spot_min_sell_sz(ex, underlying))
+ sellable = is_sellable_spot_coin(ex, underlying, avail_f)
+ return jsonify(
+ {
+ "ok": True,
+ "underlying": underlying,
+ "available": avail_f,
+ "min_sz": min_sz,
+ "sellable": bool(sellable),
+ }
+ )
+
@app.route("/api/hedge-plan/sell-coin", methods=["POST"])
@lr
def api_hedge_sell_coin():
- """手动卖回币:市价卖出交易账户全部可用标的币换 USDT."""
+ """手动卖回币:市价卖出交易账户全部可用标的币换 USDT(粉尘拒绝)."""
if not _hedge_enabled():
return jsonify({"ok": False, "msg": "对冲模块未启用"}), 400
ex = cfg.get("exchange_options") or cfg.get("exchange")
@@ -1294,6 +1331,27 @@ def register_hedge_plan_routes(app: Flask, cfg: dict[str, Any]) -> None:
underlying = str(data.get("underlying") or cfg.get("default_underly") or "ETH").strip().upper()
if underlying not in ("ETH", "BTC"):
return jsonify({"ok": False, "msg": "标的仅支持 ETH/BTC"}), 400
+ from lib.options.options_spot_bridge_lib import (
+ fetch_trading_coin_available,
+ is_sellable_spot_coin,
+ spot_min_sell_sz,
+ )
+
+ avail = fetch_trading_coin_available(ex, underlying)
+ try:
+ avail_f = float(avail) if avail is not None else 0.0
+ except (TypeError, ValueError):
+ avail_f = 0.0
+ if not is_sellable_spot_coin(ex, underlying, avail_f):
+ min_sz = spot_min_sell_sz(ex, underlying)
+ return jsonify(
+ {
+ "ok": False,
+ "msg": f"无足够 {underlying} 可卖(可用 {avail_f:g} · 最小 {min_sz:g})",
+ "available": avail_f,
+ "min_sz": min_sz,
+ }
+ ), 400
conn = cfg["get_db"]()
try:
from lib.options.options_db import init_options_tables
diff --git a/lib/hedge_plan/templates/hedge_plan_panel.html b/lib/hedge_plan/templates/hedge_plan_panel.html
index 55eaffd..e5df9d8 100644
--- a/lib/hedge_plan/templates/hedge_plan_panel.html
+++ b/lib/hedge_plan/templates/hedge_plan_panel.html
@@ -50,6 +50,9 @@
手动下单:开仓按卖一限价(非交易所市价)。先开期权(按张数买币并+5%滑点),再开永续。永续杠杆取 env:BTC=BTC_LEVERAGE/ETH=ALT_LEVERAGE。
出场:盈利金额(扣开平双边手续费后净利)或目标位点数二选一。平仓先期权(须买一流动性)再永续,再把币全部卖回 USDT。一账户同时仅一笔对冲仓。
开仓校验:已选合约 · 张数≥1 · 出场条件已填 · 门禁「开仓:可」· 账户有足够交易 USDT。
+ 期权链:默认显示约 5 档(平值 + 实值2 + 虚值2);勾选「展开全部」查看该到期更多行权价。
+ 账户:期权币本位 · 交易/资金计 USDT · 永续全仓 / 期权逐仓(余额见下方划转)。
+ 组合净盈亏:扣开平双边手续费;永续按平仓方向盘口均价估值。
买 Call + 永续空 · 点「选择」弹窗下单。