feat(order): show estimated RR before open across four exchanges
Add shared manual_order_rr_preview.js to fetch order_defaults after symbol and TP/SL inputs complete, display estimated profit-loss ratio before submit in price and percentage modes (and fixed RR), unified for risk and full-margin sizing. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,225 @@
|
||||
/**
|
||||
* 实盘下单:币种 + 止盈止损填完后拉 /api/order_defaults 快照,在开仓按钮前显示预估盈亏比。
|
||||
* 价格 / 百分比 / 固定盈亏比模式通用;与以损定仓、全仓杠杆计仓方式无关。
|
||||
*/
|
||||
(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 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 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 paint(rr, state) {
|
||||
const el = $("order-rr-preview");
|
||||
if (!el) return;
|
||||
const m = currentMode();
|
||||
if (m !== "price" && m !== "pct" && m !== "fixed_rr") {
|
||||
el.style.display = "none";
|
||||
return;
|
||||
}
|
||||
el.style.display = "";
|
||||
if (state === "empty") {
|
||||
el.textContent = "预估盈亏比:—";
|
||||
el.style.color = "#8fc8ff";
|
||||
return;
|
||||
}
|
||||
if (state === "loading") {
|
||||
el.textContent = "预估盈亏比:计算中…";
|
||||
el.style.color = "#8fc8ff";
|
||||
return;
|
||||
}
|
||||
if (state === "fetch_fail") {
|
||||
el.textContent = "预估盈亏比:取价失败";
|
||||
el.style.color = "#ff8f8f";
|
||||
return;
|
||||
}
|
||||
if (rr === null) {
|
||||
el.textContent = "预估盈亏比:无效";
|
||||
el.style.color = "#ff8f8f";
|
||||
return;
|
||||
}
|
||||
el.textContent = "预估盈亏比:" + formatRr(rr);
|
||||
el.style.color = rr >= minRr ? "#4cd97f" : "#ff8f8f";
|
||||
}
|
||||
|
||||
function refreshNow() {
|
||||
const m = currentMode();
|
||||
if (!inputsComplete(m)) {
|
||||
paint(null, "empty");
|
||||
return;
|
||||
}
|
||||
|
||||
const sym = currentSymbol();
|
||||
const dir = currentDirection();
|
||||
const seq = ++fetchSeq;
|
||||
paint(null, "loading");
|
||||
|
||||
fetch(
|
||||
"/api/order_defaults?symbol=" +
|
||||
encodeURIComponent(sym) +
|
||||
"&direction=" +
|
||||
encodeURIComponent(dir)
|
||||
)
|
||||
.then(function (r) {
|
||||
return r.json();
|
||||
})
|
||||
.then(function (data) {
|
||||
if (seq !== fetchSeq) return;
|
||||
if (!data.ok) {
|
||||
paint(null, "fetch_fail");
|
||||
return;
|
||||
}
|
||||
if (m === "pct") {
|
||||
const rr = calcRrFromPct(
|
||||
$("order-sl-pct") && $("order-sl-pct").value,
|
||||
$("order-tp-pct") && $("order-tp-pct").value
|
||||
);
|
||||
paint(rr, rr === null ? "invalid" : "ok");
|
||||
return;
|
||||
}
|
||||
const entry = num(data.last_price != null ? data.last_price : data.price);
|
||||
if (entry === null) {
|
||||
paint(null, "fetch_fail");
|
||||
return;
|
||||
}
|
||||
let rr = null;
|
||||
if (m === "fixed_rr") {
|
||||
const sl = num($("order-sl") && $("order-sl").value);
|
||||
const fixed = num($("order-fixed-rr") && $("order-fixed-rr").value);
|
||||
const tp = calcTpFromFixedRr(dir, entry, sl, fixed);
|
||||
rr = tp !== null ? fixed : null;
|
||||
} else {
|
||||
const sl = num($("order-sl") && $("order-sl").value);
|
||||
const tp = num($("order-tp") && $("order-tp").value);
|
||||
rr = calcRr(dir, entry, sl, tp);
|
||||
}
|
||||
paint(rr, rr === null ? "invalid" : "ok");
|
||||
})
|
||||
.catch(function () {
|
||||
if (seq !== fetchSeq) return;
|
||||
paint(null, "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",
|
||||
].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,
|
||||
formatRr: formatRr,
|
||||
};
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
Reference in New Issue
Block a user