/** * 实盘下单:填完币种与止盈止损后,在表单下方显示预估风险 / 预估盈利 / 预估盈亏比。 * 以损定仓:风险 = 当前交易基数 × risk%。 * 全仓杠杆:风险 = 可用保证金×缓冲 × 杠杆 × |SL-入场|/入场(与开仓 calc_risk_amount_from_plan 一致)。 */ (function (global) { "use strict"; let debounceMs = 400; let minRr = 1.5; let debounceTimer = null; let fetchSeq = 0; function $(id) { return document.getElementById(id); } function num(v) { const n = Number(v); return Number.isFinite(n) ? n : null; } function formatRr(rr) { if (rr === null || typeof rr === "undefined") return "—"; const n = Number(rr); if (!Number.isFinite(n)) return "—"; const body = Number.isInteger(n) ? String(n) : String(parseFloat(n.toFixed(2))); return body + ":1"; } function formatU(v) { if (v === null || typeof v === "undefined" || !Number.isFinite(Number(v))) return "—"; return Number(v).toFixed(2) + "U"; } function setMetric(el, label, valueText) { if (!el) return; el.innerHTML = label + ":" + valueText + ""; } function sizingMode() { return (document.body && document.body.getAttribute("data-position-sizing-mode")) || "risk"; } function isFullMarginMode() { return sizingMode() === "full_margin"; } function fullMarginBuffer() { const n = Number(document.body && document.body.getAttribute("data-full-margin-buffer")); return Number.isFinite(n) && n > 0 ? n : 0.9; } function leverageForSymbol(sym) { const u = (sym || "").trim().toUpperCase(); const btc = Number(document.body && document.body.getAttribute("data-btc-leverage")); const alt = Number(document.body && document.body.getAttribute("data-alt-leverage")); if (u.startsWith("BTC") || u.startsWith("ETH")) { return Number.isFinite(btc) && btc > 0 ? btc : 10; } return Number.isFinite(alt) && alt > 0 ? alt : 5; } function riskPercent() { const form = $("add-order-form"); const raw = (form && form.getAttribute("data-risk-percent")) || (document.body && document.body.getAttribute("data-risk-percent")) || ""; const n = Number(raw); return Number.isFinite(n) && n > 0 ? n : 1; } function calcRiskFraction(direction, entry, sl) { const e = num(entry); const s = num(sl); if (e === null || s === null || e <= 0 || s <= 0) return null; let risk = 0; if (direction === "short") { risk = s - e; } else { risk = e - s; } if (risk <= 0) return null; return risk / e; } function calcRr(direction, entry, sl, tp) { const e = num(entry); const s = num(sl); const t = num(tp); if (e === null || s === null || t === null) return null; if (direction === "short") { if (s <= e || t >= e) return null; return (e - t) / (s - e); } if (s >= e || t <= e) return null; return (t - e) / (e - s); } function calcRrFromPct(slPct, tpPct) { const sl = num(slPct); const tp = num(tpPct); if (sl === null || tp === null || sl <= 0 || tp <= 0) return null; return tp / sl; } function calcTpFromFixedRr(direction, entry, sl, rr) { const e = num(entry); const s = num(sl); const r = num(rr); if (e === null || s === null || r === null || r <= 0) return null; if (direction === "short") { if (s <= e) return null; return e - (s - e) * r; } if (s >= e) return null; return e + (e - s) * r; } function resolveSlPrice(mode, direction, entry) { if (mode === "pct") { const slPct = num($("order-sl-pct") && $("order-sl-pct").value); if (slPct === null || slPct <= 0) return null; if (direction === "short") return entry * (1 + slPct / 100); return entry * (1 - slPct / 100); } return num($("order-sl") && $("order-sl").value); } function currentMode() { return ($("sltp-mode") && $("sltp-mode").value) || "fixed_rr"; } function currentDirection() { return ($("order-direction") && $("order-direction").value) || "long"; } function currentSymbol() { return (($("order-symbol") && $("order-symbol").value) || "").trim(); } function inputsComplete(m) { const dir = currentDirection(); if (!currentSymbol() || !dir) return false; if (m === "pct") { const sl = num($("order-sl-pct") && $("order-sl-pct").value); const tp = num($("order-tp-pct") && $("order-tp-pct").value); return sl !== null && tp !== null && sl > 0 && tp > 0; } if (m === "fixed_rr") { const sl = num($("order-sl") && $("order-sl").value); const rr = num($("order-fixed-rr") && $("order-fixed-rr").value); return sl !== null && rr !== null && sl > 0 && rr > 0; } const sl = num($("order-sl") && $("order-sl").value); const tp = num($("order-tp") && $("order-tp").value); return sl !== null && tp !== null && sl > 0 && tp > 0; } function paintEmpty() { setMetric($("order-risk-preview"), "预估风险", "—"); setMetric($("order-profit-preview"), "预估盈利", "—"); setMetric($("order-rr-preview"), "预估盈亏比", "—"); } function paintLoading() { setMetric($("order-risk-preview"), "预估风险", "计算中…"); setMetric($("order-profit-preview"), "预估盈利", "计算中…"); setMetric($("order-rr-preview"), "预估盈亏比", "计算中…"); } function paintFail(kind) { const msg = kind === "fetch_fail" ? "取价失败" : "无效"; setMetric($("order-risk-preview"), "预估风险", msg); setMetric($("order-profit-preview"), "预估盈利", msg); setMetric($("order-rr-preview"), "预估盈亏比", msg); } function paintOk(riskU, profitU, rr) { setMetric($("order-risk-preview"), "预估风险", formatU(riskU)); setMetric($("order-profit-preview"), "预估盈利", formatU(profitU)); const rrEl = $("order-rr-preview"); const rrText = formatRr(rr); setMetric(rrEl, "预估盈亏比", rrText); if (rrEl && rr !== null && Number.isFinite(Number(rr))) { rrEl.classList.toggle("order-preview-rr-low", Number(rr) < minRr); rrEl.classList.toggle("order-preview-rr-ok", Number(rr) >= minRr); } } function plannedRiskFromRiskMode(capital) { const cap = num(capital); if (cap === null || cap <= 0) return null; return Math.round((cap * riskPercent()) / 100 * 100) / 100; } function plannedRiskFromFullMargin(availableUsdt, symbol, direction, entry, sl) { const avail = num(availableUsdt); if (avail === null || avail <= 0) return null; const slPx = num(sl); const entryPx = num(entry); if (slPx === null || entryPx === null) return null; const rf = calcRiskFraction(direction, entryPx, slPx); if (rf === null) return null; const margin = Math.round(avail * fullMarginBuffer() * 100) / 100; const lev = leverageForSymbol(symbol); return Math.round(margin * lev * rf * 100) / 100; } function resolvePreviewRr(m, dir, entry) { if (m === "pct") { return calcRrFromPct( $("order-sl-pct") && $("order-sl-pct").value, $("order-tp-pct") && $("order-tp-pct").value ); } const sl = num($("order-sl") && $("order-sl").value); if (m === "fixed_rr") { const fixed = num($("order-fixed-rr") && $("order-fixed-rr").value); if (fixed !== null && fixed > 0) return fixed; const tp = calcTpFromFixedRr(dir, entry, sl, fixed); return calcRr(dir, entry, sl, tp); } const tp = num($("order-tp") && $("order-tp").value); return calcRr(dir, entry, sl, tp); } function refreshNow() { if (!$("order-plan-preview")) return; const m = currentMode(); if (!inputsComplete(m)) { paintEmpty(); return; } const sym = currentSymbol(); const dir = currentDirection(); const seq = ++fetchSeq; paintLoading(); const defaultsP = fetch( "/api/order_defaults?symbol=" + encodeURIComponent(sym) + "&direction=" + encodeURIComponent(dir) ).then(function (r) { return r.json(); }); const capitalP = fetch("/api/account_snapshot").then(function (r) { return r.json(); }); Promise.all([defaultsP, capitalP]) .then(function (results) { if (seq !== fetchSeq) return; const data = results[0]; const account = results[1] || {}; if (!data.ok) { paintFail("fetch_fail"); return; } const entry = num(data.last_price != null ? data.last_price : data.price); if (entry === null) { paintFail("fetch_fail"); return; } const rr = resolvePreviewRr(m, dir, entry); if (rr === null) { paintFail("invalid"); return; } let riskU = null; if (isFullMarginMode()) { const slPx = resolveSlPrice(m, dir, entry); const avail = data.available_trading_usdt != null ? data.available_trading_usdt : account.available_trading_usdt; riskU = plannedRiskFromFullMargin(avail, sym, dir, entry, slPx); } else { riskU = plannedRiskFromRiskMode(account.current_capital); } if (riskU === null) { paintFail("fetch_fail"); return; } const profitU = Math.round(riskU * rr * 100) / 100; paintOk(riskU, profitU, rr); }) .catch(function () { if (seq !== fetchSeq) return; paintFail("fetch_fail"); }); } function schedule() { clearTimeout(debounceTimer); debounceTimer = setTimeout(refreshNow, debounceMs); } function wire(opts) { opts = opts || {}; if (opts.minRr != null && Number.isFinite(Number(opts.minRr))) { minRr = Number(opts.minRr); } if (opts.debounceMs != null && Number.isFinite(Number(opts.debounceMs))) { debounceMs = Number(opts.debounceMs); } [ "order-symbol", "order-direction", "sltp-mode", "order-sl", "order-tp", "order-sl-pct", "order-tp-pct", "order-fixed-rr", "order-leverage", ].forEach(function (id) { const el = $(id); if (!el || el._rrPreviewBound) return; el._rrPreviewBound = true; el.addEventListener("input", schedule); el.addEventListener("change", schedule); }); schedule(); } global.ManualOrderRrPreview = { wire: wire, schedule: schedule, refresh: refreshNow, calcRr: calcRr, calcRrFromPct: calcRrFromPct, calcRiskFraction: calcRiskFraction, formatRr: formatRr, }; })(typeof window !== "undefined" ? window : globalThis);