修复币本位报价误走USDC,中控补资金账户并隐藏零币余额
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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 = "限价买入 @ 卖一";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}`);
|
||||
};
|
||||
|
||||
@@ -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}`);
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)} <small style="font-size:12px;color:var(--muted)">U</small>`
|
||||
: "—";
|
||||
const tradeTxt = formatCoinTradingLabel(usdt, eth, btc);
|
||||
return `<div class="${rowCls}">
|
||||
<div class="stat-box"><div class="stat-label">资金账户</div><div class="stat-value">${fundTxt}</div></div>
|
||||
<div class="stat-box"><div class="stat-label">交易账户</div><div class="stat-value">${tradeTxt}</div></div>
|
||||
<div class="stat-box"><div class="stat-label">${pnlLabel}</div><div class="stat-value ${pnlCls(upnl)}">${fmt(upnl, 2)}</div></div>
|
||||
</div>`;
|
||||
@@ -3919,7 +3924,7 @@
|
||||
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}`);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user