e51d7824a7
Co-authored-by: Cursor <cursoragent@cursor.com>
341 lines
10 KiB
JavaScript
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);
|