diff --git a/lib/common/static/options_panel.js b/lib/common/static/options_panel.js index 919b470..7eecf8f 100644 --- a/lib/common/static/options_panel.js +++ b/lib/common/static/options_panel.js @@ -926,18 +926,29 @@ return Math.round(intrinsic * amt * 100) / 100; } - function estimateExpiryProfit(optType, strike, targetIdx, ethAmount, totalPremium) { + function estimateExpiryProfit(optType, strike, targetIdx, ethAmount, totalPremium, indexPx) { const value = estimateExpiryValue(optType, strike, targetIdx, ethAmount); - const prem = Number(totalPremium); + let prem = Number(totalPremium); if (value == null || !Number.isFinite(prem)) return null; + // 币本位权利金为币:与到期美元实值对比时先×指数 + if (isCoinMarginMode()) { + const idx = Number(indexPx); + if (!Number.isFinite(idx) || idx <= 0) return null; + prem = prem * idx; + } return Math.round((value - prem) * 100) / 100; } /** 盈亏比 = 盈利金额 / 本合约权利金(目标位仅作到期实值参考). */ - function estimateProfitRr(profit, totalPremium) { + function estimateProfitRr(profit, totalPremium, indexPx) { const pnl = Number(profit); - const prem = Number(totalPremium); + let prem = Number(totalPremium); if (!Number.isFinite(pnl) || !Number.isFinite(prem) || prem <= 0) return null; + if (isCoinMarginMode()) { + const idx = Number(indexPx); + if (!Number.isFinite(idx) || idx <= 0) return null; + prem = prem * idx; + } return Math.round((pnl / prem) * 100) / 100; } @@ -1033,18 +1044,22 @@ } } else { const value = estimateExpiryValue(q.opt_type, q.strike, Number(targetRaw), ethAmount); - const profit = estimateExpiryProfit(q.opt_type, q.strike, Number(targetRaw), ethAmount, premium); - const rr = estimateProfitRr(profit, premium); + const profit = estimateExpiryProfit(q.opt_type, q.strike, Number(targetRaw), ethAmount, premium, q.index_px); + const rr = estimateProfitRr(profit, premium, q.index_px); if (value == null || Number.isNaN(value)) { valueEl.textContent = "—"; } else { - valueEl.textContent = fmtUsdc(value) + " USDC"; + valueEl.textContent = isCoinMarginMode() + ? (fmtUsdc(value) + " U(估)") + : (fmtUsdc(value) + " USDC"); } if (profit == null || Number.isNaN(profit)) { profitEl.textContent = "—"; profitEl.className = "v"; } else { - profitEl.textContent = fmtUsdcSigned(profit); + profitEl.textContent = isCoinMarginMode() + ? ((Number(profit) > 0 ? "+" : "") + fmtUsdc(profit) + " U(估)") + : fmtUsdcSigned(profit); profitEl.className = "v " + pnlCls(profit); } if (rrEl) { @@ -1264,8 +1279,12 @@ document.getElementById("opt-order-sheets").textContent = canOpen && sz.sheets != null ? sz.sheets : "—"; document.getElementById("opt-order-eth").textContent = canOpen && sz.eth_amount != null ? sz.eth_amount : "—"; updateUnderlyingLabel(); + const coinMode = isCoinMarginMode() || (d && d.options_margin_mode === "coin"); + const premCcy = (sz.premium_ccy || (coinMode ? ((d.inst_id || "").split("-")[0] || "ETH") : "USDC")).toUpperCase(); document.getElementById("opt-order-premium").textContent = - canOpen && sz.total_premium != null ? fmtUsdc(sz.total_premium) + " USDC" : "—"; + canOpen && sz.total_premium != null + ? (coinMode ? (fmt(sz.total_premium, 6) + " " + premCcy) : (fmtUsdc(sz.total_premium) + " USDC")) + : "—"; const beEl = document.getElementById("opt-order-expiry-be"); const distEl = document.getElementById("opt-order-dist-be"); if (beEl) { @@ -1278,10 +1297,12 @@ const openBtn = document.getElementById("opt-open-btn"); if (openBtn) { openBtn.disabled = !canOpen || sz.ok === false; - const coinMode = (state.chain && state.chain.margin_mode === "coin") || (state.chain && state.chain.options_margin_mode === "coin"); - const bud = state.chain && state.chain.coin_budget && state.chain.coin_budget.budget_usdt; - if (!canOpen) { - openBtn.textContent = "暂无卖一深度,无法开仓"; + const bud = (d && d.coin_budget && d.coin_budget.budget_usdt) || + (state.chain && state.chain.coin_budget && state.chain.coin_budget.budget_usdt); + if (!canOpen || sz.ok === false) { + openBtn.textContent = coinMode + ? ((d && d.msg) || (sz && sz.msg) || "无法开仓") + : "暂无卖一深度,无法开仓"; } else if (coinMode) { openBtn.textContent = bud != null ? "买币并开仓(预算 ≈ " + Number(bud).toFixed(2) + " USDT)" : "买币并开仓 @ 卖一"; @@ -1309,6 +1330,9 @@ } else if (sz.ask_depth_capped) { msgEl.textContent = sz.msg || "已按卖一深度限制张数"; msgEl.classList.remove("opt-error"); + } else if (coinMode && sz.est_note) { + msgEl.textContent = sz.est_note; + msgEl.classList.remove("opt-error"); } else { msgEl.textContent = ""; msgEl.classList.remove("opt-error"); @@ -1504,8 +1528,20 @@ return false; } finally { const latest = state.orderQuote; + const coinMode = isCoinMarginMode() || (latest && latest.options_margin_mode === "coin"); + const bud = (latest && latest.coin_budget && latest.coin_budget.budget_usdt) || + (state.chain && state.chain.coin_budget && state.chain.coin_budget.budget_usdt); btn.disabled = !(latest && latest.ok && latest.can_open && !(latest.sizing && latest.sizing.ok === false)); - btn.textContent = (latest && latest.can_open) ? "限价买入 @ 卖一" : "暂无卖一深度,无法开仓"; + if (!(latest && latest.can_open) || (latest && latest.sizing && latest.sizing.ok === false)) { + btn.textContent = coinMode + ? ((latest && (latest.msg || (latest.sizing && latest.sizing.msg))) || "无法开仓") + : "暂无卖一深度,无法开仓"; + } else if (coinMode) { + btn.textContent = + bud != null ? "买币并开仓(预算 ≈ " + Number(bud).toFixed(2) + " USDT)" : "买币并开仓 @ 卖一"; + } else { + btn.textContent = "限价买入 @ 卖一"; + } } } diff --git a/lib/instance/instance_embed_context_lib.py b/lib/instance/instance_embed_context_lib.py index c695b1e..e00f65e 100644 --- a/lib/instance/instance_embed_context_lib.py +++ b/lib/instance/instance_embed_context_lib.py @@ -123,7 +123,7 @@ def _fmt_coin_amount(v: float | None) -> str | None: n = float(v) except (TypeError, ValueError): return None - if abs(n) < 1e-12: + if n <= 0: return None txt = f"{n:.6f}".rstrip("0").rstrip(".") return txt or None diff --git a/lib/instance/templates/embed_boot_scripts.html b/lib/instance/templates/embed_boot_scripts.html index db2edf3..fb28dc1 100644 --- a/lib/instance/templates/embed_boot_scripts.html +++ b/lib/instance/templates/embed_boot_scripts.html @@ -1158,7 +1158,7 @@ function formatTradingAccountLabel(usdt, eth, btc, marginMode) { const pushCoin = (v, ccy) => { if (v === null || v === undefined || v === "") return; const n = Number(v); - if (Number.isNaN(n) || Math.abs(n) < 1e-12) return; + if (Number.isNaN(n) || !(n > 0)) return; const txt = String(n.toFixed(6)).replace(/\.?0+$/, ""); parts.push(`${txt || "0"} ${ccy}`); }; diff --git a/lib/instance/templates/index.html b/lib/instance/templates/index.html index b6466ad..e2d9374 100644 --- a/lib/instance/templates/index.html +++ b/lib/instance/templates/index.html @@ -1639,7 +1639,7 @@ function formatTradingAccountLabel(usdt, eth, btc, marginMode) { const pushCoin = (v, ccy) => { if (v == null || v === "") return; const n = Number(v); - if (Number.isNaN(n) || Math.abs(n) < 1e-12) return; + if (Number.isNaN(n) || !(n > 0)) return; const txt = String(n.toFixed(6)).replace(/\.?0+$/, ""); parts.push(`${txt || "0"} ${ccy}`); }; diff --git a/lib/options/options_register.py b/lib/options/options_register.py index cd928a8..4c63a5f 100644 --- a/lib/options/options_register.py +++ b/lib/options/options_register.py @@ -535,6 +535,173 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None: target = sheet_count if sheet_count is not None else 0 return jsonify(_attach_close_preview(cfg, ex, {**q, "pos": target, "premium_paid": paid}, sheets=target, premium_paid=paid)) mode, mode_note = _normalize_size_mode(mode) + + # 币本位:报价预览走 USDT 预算→估币→张数,禁止再查 USDC + try: + from lib.options.options_margin_mode_lib import ( + calc_sheets_from_coin_balance, + is_coin_margin_mode, + margin_mode_from_inst_id, + ) + from lib.options.options_coin_open_lib import coin_budget_preview + from lib.exchange.okx_options_lib import cap_option_buy_sheets_to_ask_depth, option_buy_liquidity_ok + + if is_coin_margin_mode(): + ask = q.get("ask") + ask_sz = q.get("ask_sz") + can_open, block_msg = option_buy_liquidity_ok(ask, ask_sz) + try: + from lib.hedge_plan.okx_trade_mode_lib import block_standalone_open_by_mode_msg + + mode_block = block_standalone_open_by_mode_msg() + except Exception as e: + return jsonify({"ok": False, "can_open": False, "msg": f"交易模式校验失败: {e}"}) + if mode_block: + return jsonify( + { + **q, + "ok": True, + "can_open": False, + "msg": mode_block, + "options_margin_mode": "coin", + "sizing": {"ok": False, "msg": mode_block, "sheets": 0, "eth_amount": 0.0, "total_premium": 0.0}, + } + ) + if margin_mode_from_inst_id(inst_id) != "coin": + return jsonify( + { + **q, + "ok": True, + "can_open": False, + "msg": "当前为币本位模式,请选择 ETH-USD / BTC-USD 合约(非 USD_UM)", + "options_margin_mode": "coin", + "sizing": { + "ok": False, + "msg": "合约非币本位", + "sheets": 0, + "eth_amount": 0.0, + "total_premium": 0.0, + }, + } + ) + budget_info = coin_budget_preview(cfg, ex) + if not can_open: + return jsonify( + { + **q, + "ok": True, + "can_open": False, + "msg": block_msg or q.get("open_block_msg") or "暂无卖一深度,无法买入", + "options_margin_mode": "coin", + "coin_budget": budget_info, + "sizing": { + "ok": False, + "msg": block_msg or "暂无卖一深度,无法买入", + "sheets": 0, + "eth_amount": 0.0, + "total_premium": 0.0, + }, + } + ) + if not budget_info.get("ok"): + return jsonify( + { + **q, + "ok": True, + "can_open": False, + "msg": budget_info.get("msg") or "交易账户 USDT 不足", + "options_margin_mode": "coin", + "coin_budget": budget_info, + "sizing": { + "ok": False, + "msg": budget_info.get("msg") or "交易账户 USDT 不足", + "sheets": 0, + "eth_amount": 0.0, + "total_premium": 0.0, + }, + } + ) + idx = _safe_float(q.get("index_px")) or _safe_float(q.get("idxPx")) + budget_usdt = float(budget_info["budget_usdt"]) + est_coin = (budget_usdt / float(idx)) if idx and float(idx) > 0 else None + if est_coin is None or est_coin <= 0: + return jsonify( + { + **q, + "ok": True, + "can_open": False, + "msg": "无法用指数估算可买币量", + "options_margin_mode": "coin", + "coin_budget": budget_info, + "sizing": { + "ok": False, + "msg": "缺少指数价,无法预估张数", + "sheets": 0, + "eth_amount": 0.0, + "total_premium": 0.0, + }, + } + ) + sizing = calc_sheets_from_coin_balance( + quote_per_unit=float(ask), + ct_mult=float(ct_mult), + min_sz=int(min_sz), + coin_available=float(est_coin), + ) + if sizing.get("ok"): + capped, cap_msg = cap_option_buy_sheets_to_ask_depth( + int(sizing.get("sheets") or 0), + ask_sz, + min_sz=int(min_sz), + ) + if capped is None: + sizing = { + "ok": False, + "msg": cap_msg, + "sheets": 0, + "eth_amount": 0.0, + "total_premium": 0.0, + } + elif capped < int(sizing.get("sheets") or 0): + sizing = calc_sheets_from_coin_balance( + quote_per_unit=float(ask), + ct_mult=float(ct_mult), + min_sz=int(min_sz), + coin_available=float(capped) * float(ask) * float(ct_mult), + ) + if sizing.get("ok"): + sizing["ask_depth_capped"] = True + sizing["ask_sz"] = ask_sz + sizing["msg"] = f"已按卖一深度限制为 {capped} 张" + if sizing.get("ok"): + sizing["total_premium"] = sizing.get("coin_premium") + sizing["premium_ccy"] = (inst_id.split("-")[0] if inst_id else "ETH").upper() + sizing["est_coin"] = round(float(est_coin), 8) + sizing["est_note"] = "张数为预算估币预览;实盘按买币后可用量开满" + q = _attach_close_preview( + cfg, + ex, + q, + sheets=int(sizing.get("sheets") or 0), + premium_paid=_open_premium_paid(cfg, inst_id), + ) + return jsonify( + { + **q, + "can_open": bool(sizing.get("ok")), + "quote_per_unit": ask, + "premium_per_sheet": round(float(ask) * float(ct_mult), 8), + "sizing": sizing, + "mode": mode, + "mode_note": mode_note, + "options_margin_mode": "coin", + "coin_budget": budget_info, + "compound_full_enabled": _compound_full_enabled(), + } + ) + except Exception as e: + return jsonify({"ok": False, "msg": f"币本位报价失败: {e}"}) + budget = cfg["trade_budget"] budget_cap = cfg["trade_budget"] available_usdc = None diff --git a/manual_trading_hub/static/app.js b/manual_trading_hub/static/app.js index ad89f39..4304df0 100644 --- a/manual_trading_hub/static/app.js +++ b/manual_trading_hub/static/app.js @@ -3892,11 +3892,16 @@ const coinMode = isOpt && optMeta && (optMeta.options_margin_mode === "coin" || optMeta.margin_mode === "coin"); if (coinMode) { const bal = (optMeta && optMeta.balances) || {}; + const fundUsdt = bal.funding_usdt != null ? bal.funding_usdt : optMeta.funding_usdt; const usdt = bal.trading_usdt != null ? bal.trading_usdt : (optMeta.trading_usdt != null ? optMeta.trading_usdt : trading); const eth = bal.trading_eth; const btc = bal.trading_btc; + const fundTxt = fundUsdt != null && fundUsdt !== "" + ? `${fmt(fundUsdt, 2)} U` + : "—"; const tradeTxt = formatCoinTradingLabel(usdt, eth, btc); return `