From 0a20ee7eec15d106f20847fdd294e380e0202c7e Mon Sep 17 00:00:00 2001 From: dekun Date: Thu, 25 Jun 2026 19:19:32 +0800 Subject: [PATCH] Show estimated risk, profit, and RR below manual order form. Add a preview bar under the live order form with risk in red and profit in green; extend preview logic for all SL/TP modes across embed and standalone instances. Co-authored-by: Cursor --- crypto_monitor_binance/templates/index.html | 16 +- crypto_monitor_gate/templates/index.html | 16 +- crypto_monitor_gate_bot/templates/index.html | 16 +- crypto_monitor_okx/templates/index.html | 16 +- embed_templates/embed_boot_scripts.html | 2 - embed_templates/embed_page_fragment.html | 4 +- embed_templates/embed_shell.html | 3 +- static/instance_embed.js | 3 + static/instance_page.css | 9 + static/manual_order_rr_preview.js | 180 ++++++++++++------ .../order_plan_preview_bar.html | 5 + 11 files changed, 181 insertions(+), 89 deletions(-) create mode 100644 strategy_templates/order_plan_preview_bar.html diff --git a/crypto_monitor_binance/templates/index.html b/crypto_monitor_binance/templates/index.html index 68bb524..16a5381 100644 --- a/crypto_monitor_binance/templates/index.html +++ b/crypto_monitor_binance/templates/index.html @@ -37,6 +37,12 @@ .form-row{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:10px;align-items:center} .form-row > input:not([type=checkbox]):not([type=radio]),.form-row > select{flex:0 1 auto;width:10rem;max-width:200px;min-width:7rem} #add-order-form #sltp-mode{min-width:12.5rem;max-width:16rem;width:auto} + .order-plan-preview{display:flex;gap:18px;flex-wrap:wrap;align-items:center;margin:4px 0 10px;padding:10px 12px;background:#151a28;border:1px solid #2a3150;border-radius:8px;font-size:.85rem} + .order-preview-risk{color:#ff6b6b}.order-preview-risk strong{color:#ff8f8f;font-weight:600} + .order-preview-profit{color:#4cd97f}.order-preview-profit strong{color:#6ee7a0;font-weight:600} + .order-preview-rr{color:#cfd3ef}.order-preview-rr strong{font-weight:600;color:#dbe4ff} + .order-preview-rr.order-preview-rr-low strong{color:#ff8f8f} + .order-preview-rr.order-preview-rr-ok strong{color:#8fc8ff} .form-row > button,.form-row > label{flex:0 0 auto} .form-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:8px} /* 复盘表单:长下拉文案需可收缩,否则会撑破四列网格 */ @@ -240,7 +246,7 @@ - + {% macro period_stats(title, s) %}

{{ title }}

@@ -360,7 +366,7 @@ -
+ -
+ {% include 'order_plan_preview_bar.html' %}

实时持仓

@@ -827,7 +833,7 @@ - + - + - + - + - + {% include 'embed_boot_scripts.html' %} diff --git a/static/instance_embed.js b/static/instance_embed.js index 623ed9a..2fb63f0 100644 --- a/static/instance_embed.js +++ b/static/instance_embed.js @@ -64,6 +64,9 @@ } if (tab === "trade") { if (typeof global.refreshOrderDefaults === "function") global.refreshOrderDefaults(); + if (global.ManualOrderRrPreview && typeof global.ManualOrderRrPreview.wire === "function") { + global.ManualOrderRrPreview.wire(); + } } if (tab === "key_monitor" && global.KeyMonitorForm && typeof global.KeyMonitorForm.init === "function") { global.KeyMonitorForm.init(); diff --git a/static/instance_page.css b/static/instance_page.css index 211f0a1..9b07ed7 100644 --- a/static/instance_page.css +++ b/static/instance_page.css @@ -19,6 +19,15 @@ .form-row{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:10px;align-items:center} .form-row > input:not([type=checkbox]):not([type=radio]),.form-row > select{flex:0 1 auto;width:10rem;max-width:200px;min-width:7rem} #add-order-form #sltp-mode{min-width:12.5rem;max-width:16rem;width:auto} + .order-plan-preview{display:flex;gap:18px;flex-wrap:wrap;align-items:center;margin:4px 0 10px;padding:10px 12px;background:#151a28;border:1px solid #2a3150;border-radius:8px;font-size:.85rem} + .order-preview-risk{color:#ff6b6b} + .order-preview-risk strong{color:#ff8f8f;font-weight:600} + .order-preview-profit{color:#4cd97f} + .order-preview-profit strong{color:#6ee7a0;font-weight:600} + .order-preview-rr{color:#cfd3ef} + .order-preview-rr strong{font-weight:600;color:#dbe4ff} + .order-preview-rr.order-preview-rr-low strong{color:#ff8f8f} + .order-preview-rr.order-preview-rr-ok strong{color:#8fc8ff} .form-row > button,.form-row > label{flex:0 0 auto} .form-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:8px} /* 复盘表单:长下拉文案需可收缩,否则会撑破四列网格 */ diff --git a/static/manual_order_rr_preview.js b/static/manual_order_rr_preview.js index 85dc781..4469524 100644 --- a/static/manual_order_rr_preview.js +++ b/static/manual_order_rr_preview.js @@ -1,6 +1,5 @@ /** - * 实盘下单:币种 + 止盈止损填完后拉 /api/order_defaults 快照,在开仓按钮前显示预估盈亏比。 - * 仅价格 / 百分比模式;固定盈亏比模式沿用「预估止盈」,不显示预估盈亏比。 + * 实盘下单:填完币种与止盈止损后,在表单下方显示预估风险 / 预估盈利 / 预估盈亏比。 */ (function (global) { "use strict"; @@ -27,6 +26,26 @@ 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 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 calcRr(direction, entry, sl, tp) { const e = num(entry); const s = num(sl); @@ -47,6 +66,19 @@ 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"; } @@ -67,101 +99,128 @@ 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 hidePreview() { - const el = $("order-rr-preview"); - if (el) el.style.display = "none"; + function paintEmpty() { + setMetric($("order-risk-preview"), "预估风险", "—"); + setMetric($("order-profit-preview"), "预估盈利", "—"); + setMetric($("order-rr-preview"), "预估盈亏比", "—"); } - function paint(rr, state) { - const el = $("order-rr-preview"); - if (!el) return; - const m = currentMode(); - if (m === "fixed_rr" || (m !== "price" && m !== "pct")) { - el.style.display = "none"; - return; + 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); } - el.style.display = ""; - if (state === "empty") { - el.textContent = "预估盈亏比:—"; - el.style.color = "#8fc8ff"; - return; + } + + function plannedRiskU(capital) { + const cap = num(capital); + if (cap === null || cap <= 0) return null; + return Math.round((cap * riskPercent()) / 100 * 100) / 100; + } + + function resolvePreviewRr(m, dir, entry, data) { + if (m === "pct") { + return calcRrFromPct( + $("order-sl-pct") && $("order-sl-pct").value, + $("order-tp-pct") && $("order-tp-pct").value + ); } - if (state === "loading") { - el.textContent = "预估盈亏比:计算中…"; - el.style.color = "#8fc8ff"; - return; + 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); } - 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"; + 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 (m === "fixed_rr") { - hidePreview(); - return; - } if (!inputsComplete(m)) { - paint(null, "empty"); + paintEmpty(); return; } const sym = currentSymbol(); const dir = currentDirection(); const seq = ++fetchSeq; - paint(null, "loading"); + paintLoading(); - fetch( + const defaultsP = fetch( "/api/order_defaults?symbol=" + encodeURIComponent(sym) + "&direction=" + encodeURIComponent(dir) - ) - .then(function (r) { - return r.json(); - }) - .then(function (data) { + ).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) { - 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"); + paintFail("fetch_fail"); return; } const entry = num(data.last_price != null ? data.last_price : data.price); if (entry === null) { - paint(null, "fetch_fail"); + paintFail("fetch_fail"); return; } - const sl = num($("order-sl") && $("order-sl").value); - const tp = num($("order-tp") && $("order-tp").value); - const rr = calcRr(dir, entry, sl, tp); - paint(rr, rr === null ? "invalid" : "ok"); + const rr = resolvePreviewRr(m, dir, entry, data); + if (rr === null) { + paintFail("invalid"); + return; + } + const capital = num(account.current_capital); + const riskU = plannedRiskU(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; - paint(null, "fetch_fail"); + paintFail("fetch_fail"); }); } @@ -187,6 +246,7 @@ "order-sl-pct", "order-tp-pct", "order-fixed-rr", + "order-leverage", ].forEach(function (id) { const el = $(id); if (!el || el._rrPreviewBound) return; diff --git a/strategy_templates/order_plan_preview_bar.html b/strategy_templates/order_plan_preview_bar.html new file mode 100644 index 0000000..7e59ba7 --- /dev/null +++ b/strategy_templates/order_plan_preview_bar.html @@ -0,0 +1,5 @@ +
+ 预估风险: + 预估盈利: + 预估盈亏比: +