Files
crypto_okx/lib/common/static/manual_order_rr_preview.js
dekun a1abe159fa Initial standalone crypto_okx with one-click deploy.
Add deploy/manage.sh bootstrap for git.bz121.com/dekun/crypto_okx and point docs at this repo.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-13 20:00:59 +08:00

341 lines
10 KiB
JavaScript

/**
* 实盘下单:填完币种与止盈止损后,在表单下方显示预估风险 / 预估盈利 / 预估盈亏比.
* 以损定仓:风险 = 当前交易基数 × 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 + ":<strong>" + valueText + "</strong>";
}
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);