diff --git a/lib/common/static/hedge_plan.js b/lib/common/static/hedge_plan.js index 04895a2..442a215 100644 --- a/lib/common/static/hedge_plan.js +++ b/lib/common/static/hedge_plan.js @@ -1431,8 +1431,12 @@ } else if (acct.trading_usdc != null && acct.trading_usdc !== "") { state.tradingUsdt = Number(acct.trading_usdc); } - if (acct.funding_usdc != null && acct.funding_usdc !== "") { + if (acct.funding_usdt != null && acct.funding_usdt !== "") { + state.fundingUsdt = Number(acct.funding_usdt); + state.fundingUsdc = Number(acct.funding_usdt); + } else if (acct.funding_usdc != null && acct.funding_usdc !== "") { state.fundingUsdc = Number(acct.funding_usdc); + state.fundingUsdt = Number(acct.funding_usdc); } if (chain && chain.trade_budget_usdc != null && chain.trade_budget_usdc !== "") { state.tradeBudgetUsdc = Number(chain.trade_budget_usdc); @@ -1461,11 +1465,19 @@ const tradeEl = $("hp-oo-trading-usdc"); if (fundEl) fundEl.textContent = fmt(acct.funding_usdt != null ? acct.funding_usdt : acct.funding_usdc, 2); if (tradeEl) tradeEl.textContent = equityTxt; + const poFund = $("hp-po-funding-usdt"); + const poTrade = $("hp-po-trading-usdt"); + if (poFund) poFund.textContent = fmt(acct.funding_usdt != null ? acct.funding_usdt : acct.funding_usdc, 2); + if (poTrade) poTrade.textContent = equityTxt; + state.fundingEth = acct.funding_eth != null ? Number(acct.funding_eth) : null; + state.tradingEth = acct.trading_eth != null ? Number(acct.trading_eth) : null; + state.fundingBtc = acct.funding_btc != null ? Number(acct.funding_btc) : null; + state.tradingBtc = acct.trading_btc != null ? Number(acct.trading_btc) : null; autoFillOoSheets(); } - function setOoXferMsg(text, kind) { - const el = $("hp-oo-xfer-msg"); + function setXferMsg(prefix, text, kind) { + const el = $(prefix + "-xfer-msg"); if (!el) return; el.textContent = text || ""; el.classList.toggle("is-err", kind === "err"); @@ -1473,31 +1485,131 @@ el.classList.toggle("muted", !kind); } + function setOoXferMsg(text, kind) { + setXferMsg("hp-oo", text, kind); + } + + function setPoXferMsg(text, kind) { + setXferMsg("hp-po", text, kind); + } + function ooXferDir() { const sel = $("hp-oo-xfer-dir"); return (sel && sel.value) || "funding_to_trading"; } - async function submitOoUsdcTransfer(amount) { - const dir = ooXferDir(); + function poXferDir() { + const sel = $("hp-po-xfer-dir"); + return (sel && sel.value) || "funding_to_trading"; + } + + function poXferCcy() { + const sel = $("hp-po-xfer-ccy"); + return ((sel && sel.value) || "USDT").toUpperCase(); + } + + function xferAvailFor(ccy, fromAcct) { + const c = String(ccy || "USDT").toUpperCase(); + const from = String(fromAcct || "funding").toLowerCase(); + if (c === "USDT") { + if (from === "trading") { + return Number( + state.tradingUsdt != null + ? state.tradingUsdt + : state.tradingUsdc != null + ? state.tradingUsdc + : 0 + ); + } + return Number( + state.fundingUsdt != null + ? state.fundingUsdt + : state.fundingUsdc != null + ? state.fundingUsdc + : 0 + ); + } + if (c === "ETH") { + return Number(from === "trading" ? state.tradingEth || 0 : state.fundingEth || 0); + } + if (c === "BTC") { + return Number(from === "trading" ? state.tradingBtc || 0 : state.fundingBtc || 0); + } + return 0; + } + + async function submitHedgeTransfer(opts) { + const prefix = opts.prefix || "hp-oo"; + const dir = opts.dir || "funding_to_trading"; + const ccy = String(opts.ccy || "USDT").toUpperCase(); + const amount = Number(opts.amount || 0); + const setMsg = prefix === "hp-po" ? setPoXferMsg : setOoXferMsg; const from = dir === "trading_to_funding" ? "trading" : "funding"; const to = dir === "trading_to_funding" ? "funding" : "trading"; - setOoXferMsg("划转中…", null); + if (!(amount > 0)) { + setMsg("请输入有效数量", "err"); + return; + } + setMsg("划转中…", null); try { - const d = await apiJson("/api/options/transfer", { + const d = await apiJson("/api/hedge-plan/transfer", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ ccy: "USDT", from: from, to: to, amount: amount }), + body: JSON.stringify({ ccy: ccy, from: from, to: to, amount: amount }), }); if (!d.ok) { - setOoXferMsg(d.msg || "划转失败", "err"); + setMsg(d.msg || "划转失败", "err"); return; } - setOoXferMsg("划转成功", "ok"); - if ($("hp-oo-xfer-amount")) $("hp-oo-xfer-amount").value = ""; + setMsg("划转成功", "ok"); + const amtEl = $(prefix + "-xfer-amount"); + if (amtEl) amtEl.value = ""; await loadChain(); } catch (e) { - setOoXferMsg(e.message || "划转失败", "err"); + setMsg(e.message || "划转失败", "err"); + } + } + + async function submitOoUsdcTransfer(amount) { + return submitHedgeTransfer({ prefix: "hp-oo", dir: ooXferDir(), ccy: "USDT", amount: amount }); + } + + function setSellCoinMsg(text, kind) { + const el = $("hp-sell-coin-msg"); + if (!el) return; + el.textContent = text || ""; + el.classList.toggle("is-err", kind === "err"); + el.classList.toggle("is-ok", kind === "ok"); + el.classList.toggle("muted", !kind); + } + + async function submitSellCoin() { + const uly = String(state.underlying || "ETH").toUpperCase(); + if (!window.confirm("确认市价卖出交易账户全部可用 " + uly + " 换回 USDT?")) return; + setSellCoinMsg("卖回中…", null); + const btn = $("hp-sell-coin-btn"); + if (btn) btn.disabled = true; + try { + const d = await apiJson("/api/hedge-plan/sell-coin", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ underlying: uly }), + }); + if (!d.ok) { + setSellCoinMsg(d.msg || "卖回失败", "err"); + return; + } + const sold = + d.sell && d.sell.coin_sold != null + ? "已卖 " + d.sell.coin_sold + " " + uly + : d.msg || "卖回成功"; + setSellCoinMsg(sold, "ok"); + await loadChain(); + void loadLivePositions(); + } catch (e) { + setSellCoinMsg(e.message || "卖回失败", "err"); + } finally { + if (btn) btn.disabled = false; } } @@ -2901,18 +3013,14 @@ if ($("hp-oo-xfer-btn")) { $("hp-oo-xfer-btn").addEventListener("click", function () { const amount = Number(($("hp-oo-xfer-amount") && $("hp-oo-xfer-amount").value) || 0); - if (!(amount > 0)) { - setOoXferMsg("请输入有效数量", "err"); - return; - } - void submitOoUsdcTransfer(amount); + void submitHedgeTransfer({ prefix: "hp-oo", dir: ooXferDir(), ccy: "USDT", amount: amount }); }); } if ($("hp-oo-xfer-all")) { $("hp-oo-xfer-all").addEventListener("click", function () { const dir = ooXferDir(); - const max = - dir === "trading_to_funding" ? Number(state.tradingUsdc || 0) : Number(state.fundingUsdc || 0); + const from = dir === "trading_to_funding" ? "trading" : "funding"; + const max = xferAvailFor("USDT", from); if (!(max > 0)) { setOoXferMsg("划出账户可用余额不足", "err"); return; @@ -2925,11 +3033,49 @@ amtEl.removeAttribute("readonly"); amtEl.value = String(amt); } - void submitOoUsdcTransfer(amt); + void submitHedgeTransfer({ prefix: "hp-oo", dir: dir, ccy: "USDT", amount: amt }); + }); + } + if ($("hp-po-xfer-btn")) { + $("hp-po-xfer-btn").addEventListener("click", function () { + const amount = Number(($("hp-po-xfer-amount") && $("hp-po-xfer-amount").value) || 0); + void submitHedgeTransfer({ + prefix: "hp-po", + dir: poXferDir(), + ccy: poXferCcy(), + amount: amount, + }); + }); + } + if ($("hp-po-xfer-all")) { + $("hp-po-xfer-all").addEventListener("click", function () { + const dir = poXferDir(); + const ccy = poXferCcy(); + const from = dir === "trading_to_funding" ? "trading" : "funding"; + const max = xferAvailFor(ccy, from); + if (!(max > 0)) { + setPoXferMsg("划出账户可用余额不足", "err"); + return; + } + const amt = ccy === "USDT" ? Math.floor(max * 100) / 100 : Math.floor(max * 1e6) / 1e6; + const label = dir === "trading_to_funding" ? "交易 → 资金" : "资金 → 交易"; + if (!window.confirm("确认全部划转?\n币种:" + ccy + "\n方向:" + label + "\n数量:" + amt)) return; + const amtEl = $("hp-po-xfer-amount"); + if (amtEl) { + amtEl.removeAttribute("readonly"); + amtEl.value = String(amt); + } + void submitHedgeTransfer({ prefix: "hp-po", dir: dir, ccy: ccy, amount: amt }); + }); + } + if ($("hp-sell-coin-btn")) { + $("hp-sell-coin-btn").addEventListener("click", function () { + void submitSellCoin(); }); } hardenAmountAutofill([ "hp-oo-xfer-amount", + "hp-po-xfer-amount", "hp-entry", "hp-contracts", "hp-tp", diff --git a/lib/common/static/instance_theme.css b/lib/common/static/instance_theme.css index 4b0abe8..bf2b47a 100644 --- a/lib/common/static/instance_theme.css +++ b/lib/common/static/instance_theme.css @@ -3864,12 +3864,13 @@ html[data-theme="light"] .opt-be-dist-down { .hedge-plan-page-wrap #hp-po-fields-option-primary[hidden] { display: none !important; } +/* 默认约 5 档全部展开,不设滚动框(展开全部时随内容增高) */ .hedge-plan-page-wrap .hp-strike-table-wrap--6 { - max-height: 280px; - min-height: 200px; - overflow-y: auto; + max-height: none; + min-height: 0; + overflow: visible; margin-top: 4px; - flex: 1 1 auto; + flex: 0 0 auto; } .hedge-plan-page-wrap .hp-po-field { display: flex; @@ -4101,13 +4102,13 @@ html[data-theme="light"] .opt-be-dist-down { min-height: 26px; font-size: 0.72rem; } -/* 永期期权列表:视口约 5 行 + 表头,超出滚动 */ +/* 永期期权列表:默认档位全部展开,无下滑框 */ .hedge-plan-page-wrap .hp-strike-table-wrap--5 { - max-height: 248px; - min-height: 248px; - overflow-y: auto; + max-height: none; + min-height: 0; + overflow: visible; margin-top: 4px; - flex: 1 1 auto; + flex: 0 0 auto; } /* 期期 T 型:默认 3+3 一页展示,无下滑框 */ .hedge-plan-page-wrap .hp-oo-table-wrap { @@ -4342,8 +4343,8 @@ html[data-theme="light"] .opt-be-dist-down { } .hedge-plan-page-wrap .hp-po-left-stack > .hp-opt-card .hp-strike-table-wrap--6, .hedge-plan-page-wrap .hp-po-select-card .hp-strike-table-wrap--6 { - flex: 1 1 auto; - min-height: 220px; + flex: 0 0 auto; + min-height: 0; } .hedge-plan-page-wrap .hp-po-pair-hint, .hedge-plan-page-wrap .hp-po-chain-hint { diff --git a/lib/hedge_plan/hedge_plan_register.py b/lib/hedge_plan/hedge_plan_register.py index 1f3fdf6..bc5aba2 100644 --- a/lib/hedge_plan/hedge_plan_register.py +++ b/lib/hedge_plan/hedge_plan_register.py @@ -1244,6 +1244,80 @@ def register_hedge_plan_routes(app: Flask, cfg: dict[str, Any]) -> None: conn.close() return jsonify(out) + @app.route("/api/hedge-plan/transfer", methods=["POST"]) + @lr + def api_hedge_transfer(): + """账户内划转(资金↔交易);USDT/ETH/BTC.本仓不依赖单独期权 UI.""" + if not _hedge_enabled(): + return jsonify({"ok": False, "msg": "对冲模块未启用"}), 400 + ex = cfg.get("exchange_options") or cfg.get("exchange") + if ex is None: + return jsonify({"ok": False, "msg": "交易所未配置"}), 400 + data = request.get_json(silent=True) or {} + ccy = str(data.get("ccy") or "USDT").strip().upper() + if ccy not in ("USDT", "ETH", "BTC"): + return jsonify({"ok": False, "msg": "币种仅支持 USDT/ETH/BTC"}), 400 + from_acct = str(data.get("from") or "funding").strip().lower() + to_acct = str(data.get("to") or "trading").strip().lower() + if from_acct not in ("funding", "trading") or to_acct not in ("funding", "trading"): + return jsonify({"ok": False, "msg": "账户仅支持 funding/trading"}), 400 + if from_acct == to_acct: + return jsonify({"ok": False, "msg": "划出与划入账户不能相同"}), 400 + try: + amount = float(data.get("amount")) + except (TypeError, ValueError): + return jsonify({"ok": False, "msg": "数量无效"}), 400 + if amount <= 0: + return jsonify({"ok": False, "msg": "数量须大于 0"}), 400 + from lib.exchange.okx_options_lib import transfer_ccy + + result = transfer_ccy(ex, ccy, amount, from_acct, to_acct) + if result.get("ok"): + try: + from lib.exchange.okx_options_lib import invalidate_options_balance_cache + + invalidate_options_balance_cache() + except Exception: + pass + return jsonify(result) + + @app.route("/api/hedge-plan/sell-coin", methods=["POST"]) + @lr + def api_hedge_sell_coin(): + """手动卖回币:市价卖出交易账户全部可用标的币换 USDT.""" + if not _hedge_enabled(): + return jsonify({"ok": False, "msg": "对冲模块未启用"}), 400 + ex = cfg.get("exchange_options") or cfg.get("exchange") + if ex is None: + return jsonify({"ok": False, "msg": "交易所未配置"}), 400 + data = request.get_json(silent=True) or {} + 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 + conn = cfg["get_db"]() + try: + from lib.options.options_db import init_options_tables + from lib.options.options_spot_bridge_lib import sell_residual_after_option_flat + + init_options_tables(conn) + out = sell_residual_after_option_flat( + conn, + ex, + underlying=underlying, + inst_id=(str(data.get("inst_id") or "").strip() or None), + ) + if out.get("ok"): + conn.commit() + try: + from lib.exchange.okx_options_lib import invalidate_options_balance_cache + + invalidate_options_balance_cache() + except Exception: + pass + return jsonify(out) + finally: + conn.close() + @app.route("/api/hedge-plan/monitor-tick", methods=["POST"]) @lr def api_hedge_monitor_tick(): @@ -1479,6 +1553,10 @@ def _options_account_snapshot(cfg: dict[str, Any]) -> dict[str, Any]: "funding_usdt": pack.get("funding_usdt"), "trading_usdt_avail": pack.get("trading_usdt_avail", pack.get("trading_usdt")), "perp_margin_usdt": pack.get("perp_margin_usdt"), + "funding_eth": pack.get("funding_eth"), + "trading_eth": pack.get("trading_eth"), + "funding_btc": pack.get("funding_btc"), + "trading_btc": pack.get("trading_btc"), } ) except Exception: diff --git a/lib/hedge_plan/templates/hedge_plan_panel.html b/lib/hedge_plan/templates/hedge_plan_panel.html index fd04867..55eaffd 100644 --- a/lib/hedge_plan/templates/hedge_plan_panel.html +++ b/lib/hedge_plan/templates/hedge_plan_panel.html @@ -101,6 +101,32 @@
+
+
+ 资金 — + · + 交易 — + USDT + +
+
+ + + + + + +
+
- + diff --git a/lib/instance/templates/embed_shell.html b/lib/instance/templates/embed_shell.html index e9dce51..198909f 100644 --- a/lib/instance/templates/embed_shell.html +++ b/lib/instance/templates/embed_shell.html @@ -8,7 +8,7 @@ - + diff --git a/lib/instance/templates/index.html b/lib/instance/templates/index.html index 9719b9e..be24b4d 100644 --- a/lib/instance/templates/index.html +++ b/lib/instance/templates/index.html @@ -19,7 +19,7 @@ {{ pwa_app_name }} - +