币本位盈亏双显ETH/U(按指数换算);平仓卖币改为卖光交易户可用余额
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -897,6 +897,31 @@
|
||||
return fmtUsdc(n);
|
||||
}
|
||||
|
||||
/** 币本位盈亏双显:0.0018 ETH / 4.09U(按指数/现货价换算). */
|
||||
function spotPxOf(p) {
|
||||
const n = Number(p && (p.idx_px != null ? p.idx_px : p.idxPx != null ? p.idxPx : p.index_px));
|
||||
return Number.isFinite(n) && n > 0 ? n : null;
|
||||
}
|
||||
|
||||
function fmtCoinUsdtDual(coinAmt, spotPx, ccy, signed) {
|
||||
if (coinAmt === null || coinAmt === undefined || Number.isNaN(Number(coinAmt))) return "—";
|
||||
const n = Number(coinAmt);
|
||||
const unit = String(ccy || "ETH").toUpperCase();
|
||||
if (unit !== "ETH" && unit !== "BTC") {
|
||||
const sign = signed && n > 0 ? "+" : "";
|
||||
return sign + fmtUsdc(n) + "U";
|
||||
}
|
||||
const absCoin = Math.abs(n).toFixed(8).replace(/\.?0+$/, "") || "0";
|
||||
const coinSign = n < 0 ? "-" : signed && n > 0 ? "+" : "";
|
||||
const coinTxt = coinSign + absCoin + " " + unit;
|
||||
const px = Number(spotPx);
|
||||
if (!Number.isFinite(px) || !(px > 0)) return coinTxt;
|
||||
const u = n * px;
|
||||
const absU = Math.abs(u).toFixed(2);
|
||||
const uSign = u < 0 ? "-" : signed && u > 0 ? "+" : "";
|
||||
return coinTxt + " / " + uSign + absU + "U";
|
||||
}
|
||||
|
||||
function fmtPremiumAmtSigned(v, ccy) {
|
||||
if (v === null || v === undefined || Number.isNaN(Number(v))) return "—";
|
||||
const n = Number(v);
|
||||
@@ -904,6 +929,15 @@
|
||||
return sign + fmtPremiumAmt(n, ccy) + " " + (String(ccy || "USDC").toUpperCase());
|
||||
}
|
||||
|
||||
function fmtNetPnlDual(net, p) {
|
||||
const ccy = posPremiumCcy(p);
|
||||
if (ccy === "USDC") {
|
||||
if (net == null || Number.isNaN(Number(net))) return "—";
|
||||
return fmtUsdc(Number(net)) + "U";
|
||||
}
|
||||
return fmtCoinUsdtDual(net, spotPxOf(p), ccy, true);
|
||||
}
|
||||
|
||||
function fmtClosePreview(preview, premiumPaid, p) {
|
||||
if (!preview || preview.total_received == null) return "—";
|
||||
const ccy = posPremiumCcy(p);
|
||||
@@ -1614,7 +1648,7 @@
|
||||
const markTxt = p.mark_px != null ? fmtOptionPx(p.mark_px, tickSz) : fmtDisplay(p.mark_px_fmt);
|
||||
const netTxt = closePreview.bid_invalid || net == null
|
||||
? "—"
|
||||
: (fmtPremiumAmt(net, premCcy) + (coinPos ? (" " + premCcy) : ""));
|
||||
: fmtNetPnlDual(net, p);
|
||||
return (
|
||||
'<div class="pos-card-head">' +
|
||||
'<div class="pos-card-symbol"><strong>' + (p.inst_id || "") + '</strong>' +
|
||||
@@ -1873,7 +1907,7 @@
|
||||
? '<span class="opt-pos-bar-cd">到期 <span class="opt-expiry-cd" data-opt-exp-ms="' + expAttr + '">—</span></span>'
|
||||
: "") +
|
||||
'<span class="opt-pos-bar-pnl ' + uplCls + '">' +
|
||||
(net == null ? "—" : (fmtPremiumAmt(net, posPremiumCcy(p)) + " " + posPremiumCcy(p))) + "</span>" +
|
||||
(net == null ? "—" : fmtNetPnlDual(net, p)) + "</span>" +
|
||||
'<span class="opt-pos-bar-roi ' + uplCls + '">' +
|
||||
(roi == null ? "—" : fmt(roi, 2) + "%") + "</span>" +
|
||||
"</span>" +
|
||||
|
||||
@@ -56,6 +56,39 @@
|
||||
return fmtUsdc(n);
|
||||
}
|
||||
|
||||
function spotPxOf(p) {
|
||||
const n = Number(p && (p.idx_px != null ? p.idx_px : p.idxPx != null ? p.idxPx : p.index_px));
|
||||
return Number.isFinite(n) && n > 0 ? n : null;
|
||||
}
|
||||
|
||||
function fmtCoinUsdtDual(coinAmt, spotPx, ccy, signed) {
|
||||
if (coinAmt === null || coinAmt === undefined || Number.isNaN(Number(coinAmt))) return "—";
|
||||
const n = Number(coinAmt);
|
||||
const unit = String(ccy || "ETH").toUpperCase();
|
||||
if (unit !== "ETH" && unit !== "BTC") {
|
||||
const sign = signed && n > 0 ? "+" : "";
|
||||
return sign + fmtUsdc(n) + "U";
|
||||
}
|
||||
const absCoin = Math.abs(n).toFixed(8).replace(/\.?0+$/, "") || "0";
|
||||
const coinSign = n < 0 ? "-" : signed && n > 0 ? "+" : "";
|
||||
const coinTxt = coinSign + absCoin + " " + unit;
|
||||
const px = Number(spotPx);
|
||||
if (!Number.isFinite(px) || !(px > 0)) return coinTxt;
|
||||
const u = n * px;
|
||||
const absU = Math.abs(u).toFixed(2);
|
||||
const uSign = u < 0 ? "-" : signed && u > 0 ? "+" : "";
|
||||
return coinTxt + " / " + uSign + absU + "U";
|
||||
}
|
||||
|
||||
function fmtNetPnlDual(net, p) {
|
||||
const ccy = posPremiumCcy(p);
|
||||
if (ccy === "USDC") {
|
||||
if (net == null || Number.isNaN(Number(net))) return "—";
|
||||
return fmtUsdc(Number(net)) + "U";
|
||||
}
|
||||
return fmtCoinUsdtDual(net, spotPxOf(p), ccy, true);
|
||||
}
|
||||
|
||||
function optTypeLabel(t) {
|
||||
return (t || "").toUpperCase() === "P" ? "看跌 Put" : "看涨 Call";
|
||||
}
|
||||
@@ -209,7 +242,7 @@
|
||||
const pnlCells = hidePnl
|
||||
? ""
|
||||
: '<div class="pos-cell"><span class="pos-label">净盈亏</span><span class="pos-value ' + uplCls + '">' +
|
||||
(net == null ? "—" : (fmtPremiumAmt(net, premCcy) + (premCcy !== "USDC" ? (" " + premCcy) : ""))) + "</span></div>" +
|
||||
(net == null ? "—" : fmtNetPnlDual(net, p)) + "</span></div>" +
|
||||
'<div class="pos-cell"><span class="pos-label">收益率</span><span class="pos-value ' + uplCls + '">' +
|
||||
(roi == null ? "—" : fmt(roi, 2) + "%") + "</span></div>";
|
||||
return (
|
||||
|
||||
@@ -1075,7 +1075,7 @@ function refreshOrderDefaults(){
|
||||
}).catch(()=>{});
|
||||
}
|
||||
|
||||
function paintRealtimePnl(v, unit){
|
||||
function paintRealtimePnl(v, unit, spotPx){
|
||||
const nodes = document.querySelectorAll('[data-funds-field="realtime-pnl"]');
|
||||
if(!nodes.length) return;
|
||||
if(v === null || v === undefined || Number.isNaN(Number(v))){
|
||||
@@ -1088,11 +1088,23 @@ function paintRealtimePnl(v, unit){
|
||||
const n = Number(v);
|
||||
const u = String(unit || lastRealtimePnlUnit || "U").toUpperCase() || "U";
|
||||
lastRealtimePnlUnit = u;
|
||||
if (spotPx != null && Number.isFinite(Number(spotPx)) && Number(spotPx) > 0) {
|
||||
lastRealtimePnlSpotPx = Number(spotPx);
|
||||
}
|
||||
const sign = n > 0 ? "+" : "";
|
||||
let text;
|
||||
if (u === "ETH" || u === "BTC") {
|
||||
const abs = Math.abs(n).toFixed(8).replace(/\.?0+$/, "") || "0";
|
||||
text = `${n < 0 ? "-" : sign}${abs}${u}`;
|
||||
const coinTxt = `${n < 0 ? "-" : sign}${abs} ${u}`;
|
||||
const px = Number(spotPx != null ? spotPx : lastRealtimePnlSpotPx);
|
||||
if (Number.isFinite(px) && px > 0) {
|
||||
const uu = n * px;
|
||||
const uAbs = Math.abs(uu).toFixed(2);
|
||||
const uSign = uu < 0 ? "-" : uu > 0 ? "+" : "";
|
||||
text = `${coinTxt} / ${uSign}${uAbs}U`;
|
||||
} else {
|
||||
text = coinTxt;
|
||||
}
|
||||
} else {
|
||||
text = `${sign}${n.toFixed(2)}U`;
|
||||
}
|
||||
@@ -1104,15 +1116,16 @@ function paintRealtimePnl(v, unit){
|
||||
}
|
||||
let lastRealtimePnl = null;
|
||||
let lastRealtimePnlUnit = "U";
|
||||
function updateRealtimePnl(v, unit){
|
||||
let lastRealtimePnlSpotPx = null;
|
||||
function updateRealtimePnl(v, unit, spotPx){
|
||||
if(v != null && !Number.isNaN(Number(v))){
|
||||
lastRealtimePnl = Number(v);
|
||||
if (unit) lastRealtimePnlUnit = String(unit).toUpperCase();
|
||||
paintRealtimePnl(v, lastRealtimePnlUnit);
|
||||
paintRealtimePnl(v, lastRealtimePnlUnit, spotPx);
|
||||
return;
|
||||
}
|
||||
if(lastRealtimePnl != null) return;
|
||||
paintRealtimePnl(v, lastRealtimePnlUnit);
|
||||
paintRealtimePnl(v, lastRealtimePnlUnit, spotPx);
|
||||
}
|
||||
function sumOrdersFloatPnl(orders){
|
||||
if(!orders || !orders.length) return null;
|
||||
@@ -1143,14 +1156,23 @@ function paintRealtimePnlFromSnapshot(data){
|
||||
const opt = data.options_unrealized_pnl;
|
||||
const coinMode = String(data.options_margin_mode || "").toLowerCase() === "coin";
|
||||
const underly = String(data.options_underly || "ETH").toUpperCase() || "ETH";
|
||||
const spotPx = data.options_index_px != null ? Number(data.options_index_px) : null;
|
||||
if (coinMode && opt != null && !Number.isNaN(Number(opt))) {
|
||||
if (perp != null && Math.abs(Number(perp)) >= 0.005) {
|
||||
const optAbs = Math.abs(Number(opt)).toFixed(8).replace(/\.?0+$/, "") || "0";
|
||||
const optSign = Number(opt) > 0 ? "+" : (Number(opt) < 0 ? "-" : "");
|
||||
const perpSign = Number(perp) > 0 ? "+" : "";
|
||||
const text = `${perpSign}${Number(perp).toFixed(2)}U / ${optSign}${optAbs}${underly}`;
|
||||
let optPart = `${optSign}${optAbs} ${underly}`;
|
||||
if (Number.isFinite(spotPx) && spotPx > 0) {
|
||||
const uu = Number(opt) * spotPx;
|
||||
const uAbs = Math.abs(uu).toFixed(2);
|
||||
const uSign = uu < 0 ? "-" : uu > 0 ? "+" : "";
|
||||
optPart += ` / ${uSign}${uAbs}U`;
|
||||
}
|
||||
const text = `${perpSign}${Number(perp).toFixed(2)}U / ${optPart}`;
|
||||
lastRealtimePnl = Number(opt);
|
||||
lastRealtimePnlUnit = underly;
|
||||
lastRealtimePnlSpotPx = spotPx;
|
||||
document.querySelectorAll('[data-funds-field="realtime-pnl"]').forEach((pnlEl) => {
|
||||
pnlEl.innerText = text;
|
||||
const n = Number(opt) + Number(perp);
|
||||
@@ -1159,7 +1181,7 @@ function paintRealtimePnlFromSnapshot(data){
|
||||
});
|
||||
return;
|
||||
}
|
||||
paintRealtimePnl(opt, underly);
|
||||
paintRealtimePnl(opt, underly, spotPx);
|
||||
return;
|
||||
}
|
||||
const combined = combineRealtimeFloatPnl(perp, opt);
|
||||
|
||||
@@ -1556,7 +1556,7 @@ function refreshOrderDefaults(){
|
||||
}).catch(()=>{});
|
||||
}
|
||||
|
||||
function paintRealtimePnl(v, unit){
|
||||
function paintRealtimePnl(v, unit, spotPx){
|
||||
const nodes = document.querySelectorAll('[data-funds-field="realtime-pnl"]');
|
||||
if(!nodes.length) return;
|
||||
if(v === null || v === undefined || Number.isNaN(Number(v))){
|
||||
@@ -1569,11 +1569,23 @@ function paintRealtimePnl(v, unit){
|
||||
const n = Number(v);
|
||||
const u = String(unit || lastRealtimePnlUnit || "U").toUpperCase() || "U";
|
||||
lastRealtimePnlUnit = u;
|
||||
if (spotPx != null && Number.isFinite(Number(spotPx)) && Number(spotPx) > 0) {
|
||||
lastRealtimePnlSpotPx = Number(spotPx);
|
||||
}
|
||||
const sign = n > 0 ? "+" : "";
|
||||
let text;
|
||||
if (u === "ETH" || u === "BTC") {
|
||||
const abs = Math.abs(n).toFixed(8).replace(/\.?0+$/, "") || "0";
|
||||
text = `${n < 0 ? "-" : sign}${abs}${u}`;
|
||||
const coinTxt = `${n < 0 ? "-" : sign}${abs} ${u}`;
|
||||
const px = Number(spotPx != null ? spotPx : lastRealtimePnlSpotPx);
|
||||
if (Number.isFinite(px) && px > 0) {
|
||||
const uu = n * px;
|
||||
const uAbs = Math.abs(uu).toFixed(2);
|
||||
const uSign = uu < 0 ? "-" : uu > 0 ? "+" : "";
|
||||
text = `${coinTxt} / ${uSign}${uAbs}U`;
|
||||
} else {
|
||||
text = coinTxt;
|
||||
}
|
||||
} else {
|
||||
text = `${sign}${n.toFixed(2)}U`;
|
||||
}
|
||||
@@ -1585,15 +1597,16 @@ function paintRealtimePnl(v, unit){
|
||||
}
|
||||
let lastRealtimePnl = null;
|
||||
let lastRealtimePnlUnit = "U";
|
||||
function updateRealtimePnl(v, unit){
|
||||
let lastRealtimePnlSpotPx = null;
|
||||
function updateRealtimePnl(v, unit, spotPx){
|
||||
if(v != null && !Number.isNaN(Number(v))){
|
||||
lastRealtimePnl = Number(v);
|
||||
if (unit) lastRealtimePnlUnit = String(unit).toUpperCase();
|
||||
paintRealtimePnl(v, lastRealtimePnlUnit);
|
||||
paintRealtimePnl(v, lastRealtimePnlUnit, spotPx);
|
||||
return;
|
||||
}
|
||||
if(lastRealtimePnl != null) return;
|
||||
paintRealtimePnl(v, lastRealtimePnlUnit);
|
||||
paintRealtimePnl(v, lastRealtimePnlUnit, spotPx);
|
||||
}
|
||||
function sumOrdersFloatPnl(orders){
|
||||
if(!orders || !orders.length) return null;
|
||||
@@ -1624,15 +1637,24 @@ function paintRealtimePnlFromSnapshot(data){
|
||||
const opt = data.options_unrealized_pnl;
|
||||
const coinMode = String(data.options_margin_mode || "").toLowerCase() === "coin";
|
||||
const underly = String(data.options_underly || "ETH").toUpperCase() || "ETH";
|
||||
const spotPx = data.options_index_px != null ? Number(data.options_index_px) : null;
|
||||
if (coinMode && opt != null && !Number.isNaN(Number(opt))) {
|
||||
// 币本位期权盈亏单位为币,勿与永续 U 混加成「xxU」
|
||||
if (perp != null && Math.abs(Number(perp)) >= 0.005) {
|
||||
const optAbs = Math.abs(Number(opt)).toFixed(8).replace(/\.?0+$/, "") || "0";
|
||||
const optSign = Number(opt) > 0 ? "+" : (Number(opt) < 0 ? "-" : "");
|
||||
const perpSign = Number(perp) > 0 ? "+" : "";
|
||||
const text = `${perpSign}${Number(perp).toFixed(2)}U / ${optSign}${optAbs}${underly}`;
|
||||
let optPart = `${optSign}${optAbs} ${underly}`;
|
||||
if (Number.isFinite(spotPx) && spotPx > 0) {
|
||||
const uu = Number(opt) * spotPx;
|
||||
const uAbs = Math.abs(uu).toFixed(2);
|
||||
const uSign = uu < 0 ? "-" : uu > 0 ? "+" : "";
|
||||
optPart += ` / ${uSign}${uAbs}U`;
|
||||
}
|
||||
const text = `${perpSign}${Number(perp).toFixed(2)}U / ${optPart}`;
|
||||
lastRealtimePnl = Number(opt);
|
||||
lastRealtimePnlUnit = underly;
|
||||
lastRealtimePnlSpotPx = spotPx;
|
||||
document.querySelectorAll('[data-funds-field="realtime-pnl"]').forEach((pnlEl) => {
|
||||
pnlEl.innerText = text;
|
||||
const n = Number(opt) + Number(perp);
|
||||
@@ -1641,7 +1663,7 @@ function paintRealtimePnlFromSnapshot(data){
|
||||
});
|
||||
return;
|
||||
}
|
||||
paintRealtimePnl(opt, underly);
|
||||
paintRealtimePnl(opt, underly, spotPx);
|
||||
return;
|
||||
}
|
||||
const combined = combineRealtimeFloatPnl(perp, opt);
|
||||
|
||||
@@ -115,6 +115,24 @@ def build_options_hub_snapshot(cfg: dict[str, Any]) -> dict[str, Any]:
|
||||
)
|
||||
p["margin_mode_label"] = "币本位" if row_mode == "coin" else "USDC"
|
||||
|
||||
underly = (os.getenv("OKX_OPTIONS_DEFAULT_UNDERLY") or "ETH").strip().upper() or "ETH"
|
||||
options_index_px = None
|
||||
try:
|
||||
from lib.exchange.okx_options_lib import fetch_index_price
|
||||
|
||||
options_index_px = fetch_index_price(ex, underly)
|
||||
except Exception:
|
||||
options_index_px = None
|
||||
if options_index_px is None:
|
||||
for p in positions:
|
||||
try:
|
||||
px = float(p.get("idx_px") or p.get("idxPx") or 0)
|
||||
except (TypeError, ValueError):
|
||||
px = 0
|
||||
if px > 0:
|
||||
options_index_px = px
|
||||
break
|
||||
|
||||
coin_budget = None
|
||||
bridge_status = None
|
||||
open_bridges = []
|
||||
@@ -157,7 +175,8 @@ def build_options_hub_snapshot(cfg: dict[str, Any]) -> dict[str, Any]:
|
||||
"max_active_positions": options_max_active_positions(),
|
||||
"options_margin_mode": margin_mode,
|
||||
"options_margin_mode_label": "币本位" if margin_mode == "coin" else "USDC",
|
||||
"options_underly": (os.getenv("OKX_OPTIONS_DEFAULT_UNDERLY") or "ETH").strip().upper() or "ETH",
|
||||
"options_underly": underly,
|
||||
"options_index_px": options_index_px,
|
||||
"coin_budget": coin_budget,
|
||||
"bridge_status": bridge_status,
|
||||
"open_bridges": open_bridges,
|
||||
|
||||
@@ -221,26 +221,32 @@ def spot_market_sell_coin_to_usdt(
|
||||
underlying: str,
|
||||
coin_amount: float | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""交易账户:市价卖出标的币换 USDT.coin_amount 空则尽量卖光可用."""
|
||||
"""交易账户:市价卖出标的币换 USDT.
|
||||
|
||||
默认/推荐:coin_amount 为空 → 卖光交易账户全部可用币(全部卖出).
|
||||
传入 coin_amount 时仍不超过可用余额,且尽量按可用全额卖出(不故意留粉尘).
|
||||
"""
|
||||
ccy = (underlying or "ETH").upper()
|
||||
amt = coin_amount
|
||||
if amt is None or float(amt) <= 0:
|
||||
avail = fetch_trading_coin_available(ex, ccy)
|
||||
if avail is None or float(avail) <= 0:
|
||||
return {"ok": False, "msg": f"交易账户无可用 {ccy}"}
|
||||
amt = float(avail)
|
||||
if float(amt) <= 0:
|
||||
avail = fetch_trading_coin_available(ex, ccy)
|
||||
if avail is None or float(avail) <= 0:
|
||||
return {"ok": False, "msg": f"交易账户无可用 {ccy}"}
|
||||
avail_f = float(avail)
|
||||
# 全部卖出:以可用余额为准;若传入数量则不超过可用(开仓失败回滚用)
|
||||
if coin_amount is None or float(coin_amount) <= 0:
|
||||
sell_sz = avail_f
|
||||
else:
|
||||
sell_sz = min(float(coin_amount), avail_f)
|
||||
if sell_sz <= 0:
|
||||
return {"ok": False, "msg": f"{ccy} 数量须大于 0"}
|
||||
# 留一点粉尘避免精度拒单
|
||||
sell_sz = float(amt)
|
||||
if sell_sz > 1e-8:
|
||||
sell_sz = max(0.0, sell_sz * 0.999)
|
||||
inst_id = spot_quote_inst_id(ccy)
|
||||
try:
|
||||
# 现货卖出数量精度:截到 8 位
|
||||
# 现货卖出:向下截到 8 位,避免超过可用被拒;不再 *0.999 故意留残
|
||||
sz = f"{sell_sz:.8f}".rstrip("0").rstrip(".")
|
||||
if not sz or float(sz) <= 0:
|
||||
return {"ok": False, "msg": f"{ccy} 可卖数量过小"}
|
||||
# 二次钳制:格式化后仍不得超过可用
|
||||
if float(sz) > avail_f:
|
||||
sz = f"{avail_f:.8f}".rstrip("0").rstrip(".")
|
||||
body = {
|
||||
"instId": inst_id,
|
||||
"tdMode": "cash",
|
||||
@@ -328,13 +334,8 @@ def sell_residual_after_option_flat(
|
||||
if not underlying or str(b.get("underlying") or "").upper() == underlying.upper():
|
||||
target = b
|
||||
break
|
||||
coin_amt = None
|
||||
if target is not None:
|
||||
try:
|
||||
coin_amt = float(target.get("coin_bought") or 0) or None
|
||||
except (TypeError, ValueError):
|
||||
coin_amt = None
|
||||
sell = spot_market_sell_coin_to_usdt(ex, underlying=underlying, coin_amount=coin_amt)
|
||||
# 平仓后全部卖出交易账户可用标的币(含权利金盈亏留下的币),不按 bridge 记账量限卖
|
||||
sell = spot_market_sell_coin_to_usdt(ex, underlying=underlying, coin_amount=None)
|
||||
if target is None:
|
||||
if not sell.get("ok"):
|
||||
msg = str(sell.get("msg") or "")
|
||||
|
||||
Reference in New Issue
Block a user