币本位盈亏双显ETH/U(按指数换算);平仓卖币改为卖光交易户可用余额
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -7076,9 +7076,10 @@ def api_account_snapshot():
|
||||
options_trading_btc = None
|
||||
options_margin_mode = "coin"
|
||||
options_underly = "ETH"
|
||||
options_index_px = None
|
||||
if OKX_OPTIONS_ENABLED and exchange_options.apiKey:
|
||||
try:
|
||||
from lib.exchange.okx_options_lib import options_header_balance_pack
|
||||
from lib.exchange.okx_options_lib import fetch_index_price, options_header_balance_pack
|
||||
|
||||
_op = options_header_balance_pack(exchange_options, force=force_refresh)
|
||||
options_trading_usdc = _op.get("trading_usdc")
|
||||
@@ -7090,6 +7091,7 @@ def api_account_snapshot():
|
||||
options_trading_btc = _op.get("trading_btc")
|
||||
options_margin_mode = _op.get("options_margin_mode") or "coin"
|
||||
options_underly = _op.get("options_underly") or "ETH"
|
||||
options_index_px = fetch_index_price(exchange_options, options_underly)
|
||||
except Exception:
|
||||
options_trading_usdc = None
|
||||
options_funding_usdc = None
|
||||
@@ -7100,6 +7102,7 @@ def api_account_snapshot():
|
||||
options_trading_btc = None
|
||||
options_margin_mode = "coin"
|
||||
options_underly = "ETH"
|
||||
options_index_px = None
|
||||
recommended_capital = get_recommended_capital(current_capital)
|
||||
from lib.strategy.strategy_trade_labels import count_position_limit_active_monitors
|
||||
|
||||
@@ -7187,6 +7190,7 @@ def api_account_snapshot():
|
||||
"options_trading_btc": options_trading_btc,
|
||||
"options_margin_mode": options_margin_mode,
|
||||
"options_underly": options_underly,
|
||||
"options_index_px": options_index_px,
|
||||
"total_funds": total_funds_usdt(
|
||||
funding_usdt if _show_perp_funds else None,
|
||||
current_capital if _show_perp_funds else None,
|
||||
@@ -7586,9 +7590,14 @@ def api_price_snapshot():
|
||||
)
|
||||
|
||||
options_unrealized_pnl = None
|
||||
options_index_px = None
|
||||
options_margin_mode = None
|
||||
options_underly = None
|
||||
if OKX_OPTIONS_ENABLED and exchange_options.apiKey:
|
||||
try:
|
||||
from lib.options.options_positions_lib import sum_options_net_pnl_usdc
|
||||
from lib.options.options_margin_mode_lib import normalize_options_margin_mode
|
||||
from lib.exchange.okx_options_lib import fetch_index_price
|
||||
|
||||
opt_cfg = app.extensions.get("options_cfg")
|
||||
if opt_cfg:
|
||||
@@ -7597,6 +7606,9 @@ def api_price_snapshot():
|
||||
from lib.exchange.okx_options_lib import fetch_options_unrealized_pnl_usdc
|
||||
|
||||
options_unrealized_pnl = fetch_options_unrealized_pnl_usdc(exchange_options)
|
||||
options_margin_mode = normalize_options_margin_mode()
|
||||
options_underly = (os.getenv("OKX_OPTIONS_DEFAULT_UNDERLY") or "ETH").strip().upper() or "ETH"
|
||||
options_index_px = fetch_index_price(exchange_options, options_underly)
|
||||
except Exception:
|
||||
options_unrealized_pnl = None
|
||||
|
||||
@@ -7607,6 +7619,9 @@ def api_price_snapshot():
|
||||
"position_marks": position_marks,
|
||||
"positions_raw_count": len(all_swap_positions),
|
||||
"options_unrealized_pnl": options_unrealized_pnl,
|
||||
"options_index_px": options_index_px,
|
||||
"options_margin_mode": options_margin_mode,
|
||||
"options_underly": options_underly,
|
||||
**force_close_template_context(
|
||||
FORCE_CLOSE_ENABLED,
|
||||
FORCE_CLOSE_BJ_HOUR,
|
||||
|
||||
@@ -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 "")
|
||||
|
||||
@@ -782,13 +782,37 @@
|
||||
return fmt(n, 2);
|
||||
}
|
||||
|
||||
function fmtOptPnlText(v, ccy) {
|
||||
function spotPxFromOptMeta(optMeta, p) {
|
||||
if (p) {
|
||||
const n = Number(p.idx_px != null ? p.idx_px : p.idxPx != null ? p.idxPx : p.index_px);
|
||||
if (Number.isFinite(n) && n > 0) return n;
|
||||
}
|
||||
if (optMeta) {
|
||||
const n = Number(optMeta.options_index_px != null ? optMeta.options_index_px : optMeta.index_px);
|
||||
if (Number.isFinite(n) && n > 0) return n;
|
||||
const pos = Array.isArray(optMeta.positions) ? optMeta.positions : [];
|
||||
for (let i = 0; i < pos.length; i++) {
|
||||
const px = Number(pos[i] && (pos[i].idx_px != null ? pos[i].idx_px : pos[i].idxPx));
|
||||
if (Number.isFinite(px) && px > 0) return px;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function fmtOptPnlText(v, ccy, spotPx) {
|
||||
if (v === null || v === undefined || Number.isNaN(Number(v))) return "—";
|
||||
const n = Number(v);
|
||||
const unit = String(ccy || "USDC").toUpperCase();
|
||||
const sign = n > 0 ? "+" : "";
|
||||
if (unit === "ETH" || unit === "BTC") {
|
||||
return `${sign}${fmtOptPnlAmt(n, unit)} ${unit}`;
|
||||
const coin = `${sign}${fmtOptPnlAmt(Math.abs(n), unit)}`;
|
||||
const signedCoin = n < 0 ? `-${fmtOptPnlAmt(Math.abs(n), unit)}` : coin;
|
||||
const px = Number(spotPx);
|
||||
if (!Number.isFinite(px) || !(px > 0)) return `${signedCoin} ${unit}`;
|
||||
const u = n * px;
|
||||
const uAbs = Math.abs(u).toFixed(2);
|
||||
const uTxt = u < 0 ? `-${uAbs}` : u > 0 ? `+${uAbs}` : uAbs;
|
||||
return `${signedCoin} ${unit} / ${uTxt}U`;
|
||||
}
|
||||
return `${sign}${fmt(n, 2)}U`;
|
||||
}
|
||||
@@ -3948,9 +3972,10 @@
|
||||
: "—";
|
||||
const tradeTxt = formatCoinTradingLabel(usdt, eth, btc);
|
||||
const pnlCcy = optionsPanelCcy(optMeta);
|
||||
const spotPx = spotPxFromOptMeta(optMeta);
|
||||
const pnlTxt = upnl == null || Number.isNaN(Number(upnl))
|
||||
? "—"
|
||||
: `<span class="${pnlCls(upnl)}">${fmtOptPnlText(upnl, pnlCcy)}</span>`;
|
||||
: `<span class="${pnlCls(upnl)}">${fmtOptPnlText(upnl, pnlCcy, spotPx)}</span>`;
|
||||
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>
|
||||
@@ -4116,7 +4141,7 @@
|
||||
${renderOptionsTargetCell(target, p)}`;
|
||||
if (showPnl) {
|
||||
const premCcy = optPremiumCcyOf(p);
|
||||
html += `<td class="${pnlCls(net)}">${net == null ? "—" : fmtOptPnlText(net, premCcy)}</td>
|
||||
html += `<td class="${pnlCls(net)}">${net == null ? "—" : fmtOptPnlText(net, premCcy, spotPxFromOptMeta(null, p))}</td>
|
||||
<td class="${pnlCls(net)}">${roi == null ? "—" : esc(Number(roi).toFixed(2)) + "%"}</td>`;
|
||||
}
|
||||
html += "</tr>";
|
||||
@@ -4602,6 +4627,7 @@
|
||||
let pnlShow = upnl;
|
||||
let pnlSuffix = "";
|
||||
let pnlUnit = "U";
|
||||
let pnlSpotPx = null;
|
||||
if (hasOptCap) {
|
||||
if (opt.enabled === false) {
|
||||
optLine = "期权未启用";
|
||||
@@ -4629,13 +4655,14 @@
|
||||
parts.push(`交易 ${fmt(bal.trading, 2)}U`);
|
||||
}
|
||||
if (optUpl != null && Number.isFinite(Number(optUpl))) {
|
||||
parts.push(`浮盈 ${fmtOptPnlText(optUpl, optCcy)}`);
|
||||
parts.push(`浮盈 ${fmtOptPnlText(optUpl, optCcy, spotPxFromOptMeta(opt))}`);
|
||||
}
|
||||
if (optUpl != null && Number.isFinite(Number(optUpl)) && openCount === 0) {
|
||||
// 永续空仓时主数字优先展示期权浮盈,避免一直显示 0U
|
||||
pnlShow = optUpl;
|
||||
pnlSuffix = "期权";
|
||||
pnlUnit = optCcy;
|
||||
pnlSpotPx = spotPxFromOptMeta(opt);
|
||||
}
|
||||
}
|
||||
optLine = parts.join(" · ");
|
||||
@@ -4646,7 +4673,7 @@
|
||||
const strategyStats = renderCardStrategyStats(row, hm, flaskOk);
|
||||
const tilePnlHtml =
|
||||
pnlUnit === "ETH" || pnlUnit === "BTC"
|
||||
? `${fmtOptPnlAmt(pnlShow, pnlUnit)} <small>${esc(pnlUnit)}${pnlSuffix ? " · " + esc(pnlSuffix) : ""}</small>`
|
||||
? `${fmtOptPnlText(pnlShow, pnlUnit, pnlSpotPx)} <small>${pnlSuffix ? esc(pnlSuffix) : ""}</small>`
|
||||
: `${fmt(pnlShow, 2)} <small>U${pnlSuffix ? " · " + esc(pnlSuffix) : ""}</small>`;
|
||||
return `<div class="card hub-tile ${tileCls}" data-ex-id="${esc(row.id)}">
|
||||
<div class="hub-tile-body card-expand-zone" title="点击进入全屏详情">
|
||||
|
||||
@@ -51,14 +51,20 @@
|
||||
return "USDC";
|
||||
}
|
||||
|
||||
function pnlSignedOpt(v, ccy) {
|
||||
function pnlSignedOpt(v, ccy, spotPx) {
|
||||
const n = Number(v);
|
||||
if (!Number.isFinite(n)) return "—";
|
||||
const unit = String(ccy || "USDC").toUpperCase();
|
||||
if (unit === "ETH" || unit === "BTC") {
|
||||
const abs = Math.abs(n).toFixed(8).replace(/\.?0+$/, "") || "0";
|
||||
const sign = n > 0 ? "+" : n < 0 ? "-" : "";
|
||||
return `${sign}${abs} ${unit}`;
|
||||
const coinTxt = `${sign}${abs} ${unit}`;
|
||||
const px = Number(spotPx);
|
||||
if (!Number.isFinite(px) || !(px > 0)) return coinTxt;
|
||||
const u = n * px;
|
||||
const uAbs = Math.abs(u).toFixed(2);
|
||||
const uSign = u < 0 ? "-" : u > 0 ? "+" : "";
|
||||
return `${coinTxt} / ${uSign}${uAbs}U`;
|
||||
}
|
||||
return pnlSigned(n, 2);
|
||||
}
|
||||
@@ -361,7 +367,15 @@
|
||||
<td>${p.idx_px != null ? fmt(p.idx_px, 0) : "—"}</td>
|
||||
<td><span class="${targetCls}">${esc(target)}</span></td>`;
|
||||
if (showPnl) {
|
||||
html += `<td class="${pnlClass(net)}">${net != null ? pnlSignedOpt(net, optPremiumCcyOf(p)) : "—"}</td>
|
||||
html += `<td class="${pnlClass(net)}">${
|
||||
net != null
|
||||
? pnlSignedOpt(
|
||||
net,
|
||||
optPremiumCcyOf(p),
|
||||
Number(p.idx_px != null ? p.idx_px : p.idxPx) || null
|
||||
)
|
||||
: "—"
|
||||
}</td>
|
||||
<td class="${pnlClass(roi)}">${roi != null ? esc(Number(roi).toFixed(2)) + "%" : "—"}</td>`;
|
||||
}
|
||||
html += "</tr>";
|
||||
|
||||
Reference in New Issue
Block a user