Align hedge-plan perp contract precision with exchange and show TP/SL USDT PnL.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-14 12:49:03 +08:00
parent 036501b70c
commit 9e0d89bd01
8 changed files with 148 additions and 19 deletions
+70 -9
View File
@@ -177,6 +177,7 @@
const acctLabel = d.account_label || "合约账户";
const tag = $("hp-perp-acct-tag");
if (tag) tag.textContent = acctLabel;
const amtPrec = d.amount_precision != null ? Number(d.amount_precision) : 4;
const q = $("hp-perp-quote");
if (q) {
q.innerHTML =
@@ -186,7 +187,7 @@
acctLabel +
"可用 <strong>" +
fmt(d.available_usdt, 2) +
"</strong> U<br/>标记 <strong>" +
"</strong> USDT<br/>标记 <strong>" +
fmt(d.mark, 2) +
"</strong> · 最新 " +
fmt(d.last, 2) +
@@ -195,7 +196,15 @@
" · 买一 " +
fmt(d.bid, 2) +
" · 面值 " +
fmt(d.contract_size, 4);
fmt(d.contract_size, 4) +
" · 张精度 " +
amtPrec +
" 位";
}
const contractsInput = $("hp-contracts");
if (contractsInput) {
const step = amtPrec <= 0 ? "1" : String(Math.pow(10, -amtPrec));
contractsInput.step = step;
}
const sz = $("hp-sizing-line");
if (sz) {
@@ -206,25 +215,73 @@
acctLabel +
"):保证金 " +
fmt(s.margin_capital, 2) +
"U × " +
" USDT × " +
s.leverage +
"x → 名义 " +
fmt(s.notional_value, 2) +
"U · 建议 " +
fmt(d.suggest_contracts, 4) +
" ";
" USDT · 建议 " +
fmt(d.suggest_contracts, amtPrec) +
" 合约张(已按交易所精度)";
} else {
sz.textContent = "非全仓或不具备保证金数据时仅手动填张数;永期开仓需全仓.";
}
}
const entry = $("hp-entry");
if (entry && d.entry_ref && !entry.value) entry.value = d.entry_ref;
const contracts = $("hp-contracts");
if (contracts && d.suggest_contracts != null && !contracts.value) {
contracts.value = d.suggest_contracts;
if (contractsInput && d.suggest_contracts != null && !contractsInput.value) {
contractsInput.value = fmt(d.suggest_contracts, amtPrec);
}
const label = $("hp-opt-type-label");
if (label) label.textContent = d.suggested_opt_type === "C" ? "Call" : "Put";
updatePerpPnlHint();
}
function updatePerpPnlHint() {
const el = $("hp-perp-pnl-line");
if (!el) return;
const entry = Number(($("hp-entry") && $("hp-entry").value) || NaN);
const tp = Number(($("hp-tp") && $("hp-tp").value) || NaN);
const sl = Number(($("hp-sl") && $("hp-sl").value) || NaN);
const contracts = Number(($("hp-contracts") && $("hp-contracts").value) || NaN);
const cs = Number((state.market && state.market.contract_size) || 0.01);
const dir = (($("hp-direction") && $("hp-direction").value) || "long").toLowerCase();
if (!(entry > 0) || !(contracts > 0) || !(cs > 0)) {
el.textContent = "填写开仓价与张数后,输入止盈/止损可看永续盈亏金额(USDT)";
return;
}
function pnlAt(exitPx) {
const coins = contracts * cs;
if (dir === "short") return (entry - exitPx) * coins;
return (exitPx - entry) * coins;
}
const parts = [];
if (tp > 0) {
const p = pnlAt(tp);
parts.push(
"止盈预期 <strong class=\"" +
(p >= 0 ? "hp-pnl-pos" : "hp-pnl-neg") +
"\">" +
(p >= 0 ? "+" : "") +
fmt(p, 2) +
" USDT</strong>"
);
} else {
parts.push("止盈预期 —");
}
if (sl > 0) {
const p = pnlAt(sl);
parts.push(
"止损预期 <strong class=\"" +
(p >= 0 ? "hp-pnl-pos" : "hp-pnl-neg") +
"\">" +
(p >= 0 ? "+" : "") +
fmt(p, 2) +
" USDT</strong>"
);
} else {
parts.push("止损预期 —");
}
el.innerHTML = parts.join(" · ");
}
function fillExpSelect(sel, chain) {
@@ -632,6 +689,10 @@
});
});
}
["hp-entry", "hp-tp", "hp-sl", "hp-contracts"].forEach(function (id) {
const el = $(id);
if (el) el.addEventListener("input", updatePerpPnlHint);
});
if ($("hp-refresh"))
$("hp-refresh").addEventListener("click", function () {
void refreshAll();
+22
View File
@@ -3212,6 +3212,28 @@ html[data-theme="light"] .opt-be-dist-down {
font-weight: 500;
margin-left: 4px;
}
.hedge-plan-page-wrap .hp-unit {
font-size: 0.66rem;
color: #8892b0;
font-weight: 500;
margin-right: 2px;
}
.hedge-plan-page-wrap .hp-unit-hint {
font-size: 0.68rem;
margin: 2px 0 6px;
line-height: 1.4;
}
.hedge-plan-page-wrap #hp-perp-pnl-line {
font-size: 0.74rem;
margin: 4px 0 6px;
line-height: 1.45;
}
.hedge-plan-page-wrap .hp-pnl-pos {
color: #7ee787;
}
.hedge-plan-page-wrap .hp-pnl-neg {
color: #ff8a8a;
}
.hedge-plan-page-wrap .form-row label,
.hedge-plan-page-wrap .form-row select,
.hedge-plan-page-wrap .form-row input,
+17
View File
@@ -69,6 +69,23 @@ def suggest_contracts_from_notional(
return float(notional) / (float(entry) * float(contract_size))
def floor_contracts_to_precision(contracts: float, decimals: int) -> float:
"""按交易所张数精度向下取整,避免建议张数超过可用保证金."""
import math
raw = float(contracts or 0.0)
if raw <= 0:
return 0.0
try:
d = int(decimals)
except (TypeError, ValueError):
d = 0
if d <= 0:
return float(math.floor(raw + 1e-12))
scale = 10**d
return math.floor(raw * scale + 1e-12) / scale
def build_perp_options_preview(
*,
direction: str,
+22 -2
View File
@@ -10,10 +10,12 @@ from jinja2 import ChoiceLoader, FileSystemLoader
from lib.hedge_plan.hedge_plan_calc_lib import (
build_options_options_preview,
build_perp_options_preview,
floor_contracts_to_precision,
gate_status,
option_premium_total,
suggest_contracts_from_notional,
)
from lib.hub.hub_calculator_market_lib import amount_decimals_from_exchange
from lib.trade.position_sizing_lib import (
compute_full_margin_sizing,
load_position_sizing_mode,
@@ -305,6 +307,11 @@ def _fetch_perp_market(cfg: dict[str, Any], base: str) -> tuple[dict[str, Any],
entry = float(mark or last or 0)
sizing = None
suggest_contracts = None
amount_precision = 4
try:
amount_precision = int(amount_decimals_from_exchange(ex, sym))
except Exception:
amount_precision = 4
if available is not None and entry > 0:
sizing, _serr = compute_full_margin_sizing(
symbol=sym,
@@ -316,11 +323,20 @@ def _fetch_perp_market(cfg: dict[str, Any], base: str) -> tuple[dict[str, Any],
funds_decimals=int(cfg.get("funds_decimals") or 2),
)
if sizing:
suggest_contracts = suggest_contracts_from_notional(
raw_contracts = suggest_contracts_from_notional(
notional=float(sizing["notional_value"]),
entry=entry,
contract_size=cs,
)
# 优先走交易所 amount_to_precision;失败则按精度位数向下取整
suggest_contracts = None
try:
precise = float(ex.amount_to_precision(sym, raw_contracts))
if precise > raw_contracts + 1e-12:
precise = floor_contracts_to_precision(raw_contracts, amount_precision)
suggest_contracts = precise
except Exception:
suggest_contracts = floor_contracts_to_precision(raw_contracts, amount_precision)
return {
"exchange_symbol": sym,
@@ -331,7 +347,11 @@ def _fetch_perp_market(cfg: dict[str, Any], base: str) -> tuple[dict[str, Any],
"contract_size": cs,
"available_usdt": available,
"full_margin_sizing": sizing,
"suggest_contracts": round(suggest_contracts, 6) if suggest_contracts is not None else None,
"suggest_contracts": suggest_contracts,
"amount_precision": amount_precision,
"unit_quote": "USDT",
"unit_contracts": "合约张",
"unit_note": "价格单位 USDT;张数=交易所永续合约张(与下单精度一致);名义≈张数×面值×价格",
"entry_ref": entry or None,
}, None
@@ -39,12 +39,14 @@
</select>
</div>
<div id="hp-perp-quote" class="muted hp-quote-line">加载中…</div>
<p class="muted hp-unit-hint">单位说明:价格=USDT · 张数=交易所<strong>永续合约张</strong>(精度与 OKX 下单一致) · 盈亏=USDT</p>
<div class="form-row" style="flex-wrap:wrap">
<label>开仓价 <input type="number" step="any" id="hp-entry" /></label>
<label>止盈 <input type="number" step="any" id="hp-tp" /></label>
<label>止损 <input type="number" step="any" id="hp-sl" /></label>
<label>张数 <input type="number" step="any" id="hp-contracts" /></label>
<label>开仓价 <span class="hp-unit">USDT</span> <input type="number" step="any" id="hp-entry" /></label>
<label>止盈 <span class="hp-unit">USDT</span> <input type="number" step="any" id="hp-tp" /></label>
<label>止损 <span class="hp-unit">USDT</span> <input type="number" step="any" id="hp-sl" /></label>
<label>张数 <span class="hp-unit">合约张</span> <input type="number" step="any" id="hp-contracts" /></label>
</div>
<p class="muted" id="hp-perp-pnl-line"></p>
<p class="muted" id="hp-sizing-line"></p>
</div>
<div class="card hp-opt-card">
@@ -75,9 +77,10 @@
</div>
<div class="form-row hp-pick-row">
<label>已选 <code id="hp-sel-inst"></code></label>
<label>张数 <input type="number" step="1" min="1" id="hp-sheets" value="1" /></label>
<label>张数 <span class="hp-unit">期权张</span> <input type="number" step="1" min="1" id="hp-sheets" value="1" /></label>
<span class="muted" id="hp-premium-line"></span>
</div>
<p class="muted hp-unit-hint">单位说明:权利金结算币=<strong>USDC</strong> · 张数=期权张(整张) · 卖一/买一=价格/张</p>
<div id="hp-opt-bal-line" class="muted hp-quote-line hp-opt-bal-line"></div>
</div>
</div>
@@ -192,4 +195,4 @@
</div>
</div>
</div>
<script src="/static/hedge_plan.js?v=6"></script>
<script src="/static/hedge_plan.js?v=7"></script>
+1 -1
View File
@@ -7,7 +7,7 @@
<link rel="stylesheet" href="/static/instance_theme_early.css?v=4">
<link rel="stylesheet" href="/static/account_risk_badge.css?v=4">
<link rel="stylesheet" href="/static/instance_page.css?v=6">
<link rel="stylesheet" href="/static/instance_theme.css?v=85">
<link rel="stylesheet" href="/static/instance_theme.css?v=86">
<script src="/static/account_risk_badge.js?v=4"></script>
<meta name="theme-color" content="#0b0d14">
<title>{{ exchange_display }} · 加密货币 | 交易监控复盘系统</title>
+1 -1
View File
@@ -17,7 +17,7 @@
<link rel="manifest" href="/static/icons/manifest.webmanifest">
<title>{{ exchange_display }} · 加密货币 | 交易监控复盘系统</title>
<link rel="stylesheet" href="/static/instance_page.css?v=3">
<link rel="stylesheet" href="/static/instance_theme.css?v=85">
<link rel="stylesheet" href="/static/instance_theme.css?v=86">
</head>
<body
+6
View File
@@ -4,6 +4,7 @@ import unittest
from lib.hedge_plan.hedge_plan_calc_lib import (
build_options_options_preview,
build_perp_options_preview,
floor_contracts_to_precision,
gate_status,
option_expiry_pnl,
option_premium_total,
@@ -81,6 +82,11 @@ class TestHedgePlanCalc(unittest.TestCase):
10,
)
def test_floor_contracts_to_precision(self):
self.assertEqual(floor_contracts_to_precision(4.569713, 4), 4.5697)
self.assertEqual(floor_contracts_to_precision(4.569713, 0), 4.0)
self.assertEqual(floor_contracts_to_precision(0, 4), 0.0)
if __name__ == "__main__":
unittest.main()