From e51d7824a73bb046c1731c940e5e7905382e1f5a Mon Sep 17 00:00:00 2001 From: dekun Date: Mon, 29 Jun 2026 00:38:27 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=85=A8=E4=BB=93=E6=A8=A1=E5=BC=8F?= =?UTF-8?q?=E9=A2=84=E4=BC=B0=E9=A3=8E=E9=99=A9/=E7=9B=88=E5=88=A9?= =?UTF-8?q?=E6=8C=89=E6=9D=A0=E6=9D=86=E4=B8=8E=E5=8F=AF=E7=94=A8=E4=BF=9D?= =?UTF-8?q?=E8=AF=81=E9=87=91=E8=AE=A1=E7=AE=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- crypto_monitor_binance/templates/index.html | 11 ++- crypto_monitor_gate/templates/index.html | 11 ++- crypto_monitor_gate_bot/templates/index.html | 11 ++- crypto_monitor_okx/templates/index.html | 11 ++- docs/manual-order-rr-preview.md | 3 +- embed_templates/embed_shell.html | 6 +- static/manual_order_rr_preview.js | 82 ++++++++++++++++++-- tests/test_manual_order_rr_preview.py | 27 +++++++ 8 files changed, 147 insertions(+), 15 deletions(-) diff --git a/crypto_monitor_binance/templates/index.html b/crypto_monitor_binance/templates/index.html index ccb96c1..4f9eec1 100644 --- a/crypto_monitor_binance/templates/index.html +++ b/crypto_monitor_binance/templates/index.html @@ -246,7 +246,14 @@ - + {% macro period_stats(title, s) %}

{{ title }}

@@ -833,7 +840,7 @@ - + - + - + - + - + {% include 'embed_boot_scripts.html' %} diff --git a/static/manual_order_rr_preview.js b/static/manual_order_rr_preview.js index 4469524..49dffc7 100644 --- a/static/manual_order_rr_preview.js +++ b/static/manual_order_rr_preview.js @@ -1,5 +1,7 @@ /** * 实盘下单:填完币种与止盈止损后,在表单下方显示预估风险 / 预估盈利 / 预估盈亏比。 + * 以损定仓:风险 = 当前交易基数 × risk%。 + * 全仓杠杆:风险 = 可用保证金×缓冲 × 杠杆 × |SL-入场|/入场(与开仓 calc_risk_amount_from_plan 一致)。 */ (function (global) { "use strict"; @@ -36,6 +38,29 @@ 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 = @@ -46,6 +71,20 @@ 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); @@ -79,6 +118,16 @@ 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"; } @@ -140,13 +189,26 @@ } } - function plannedRiskU(capital) { + function plannedRiskFromRiskMode(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) { + 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, @@ -204,13 +266,22 @@ paintFail("fetch_fail"); return; } - const rr = resolvePreviewRr(m, dir, entry, data); + const rr = resolvePreviewRr(m, dir, entry); if (rr === null) { paintFail("invalid"); return; } - const capital = num(account.current_capital); - const riskU = plannedRiskU(capital); + 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; @@ -263,6 +334,7 @@ refresh: refreshNow, calcRr: calcRr, calcRrFromPct: calcRrFromPct, + calcRiskFraction: calcRiskFraction, formatRr: formatRr, }; })(typeof window !== "undefined" ? window : globalThis); diff --git a/tests/test_manual_order_rr_preview.py b/tests/test_manual_order_rr_preview.py index ef74688..bc07ff1 100644 --- a/tests/test_manual_order_rr_preview.py +++ b/tests/test_manual_order_rr_preview.py @@ -37,3 +37,30 @@ def test_invalid_geometry_returns_none(): def test_pct_mode_rr(): assert _calc_rr_from_pct(2.0, 4.0) == 2.0 assert _calc_rr_from_pct(1.5, 3.0) == 2.0 + + +def _calc_risk_fraction(direction: str, entry: float, sl: float): + if entry <= 0 or sl <= 0: + return None + if direction == "short": + risk = sl - entry + else: + risk = entry - sl + if risk <= 0: + return None + return risk / entry + + +def _full_margin_risk_u(available: float, buffer: float, leverage: int, direction: str, entry: float, sl: float): + rf = _calc_risk_fraction(direction, entry, sl) + if rf is None: + return None + margin = round(available * buffer, 2) + return round(margin * leverage * rf, 2) + + +def test_full_margin_risk_short_hype(): + # 可用约 23.06U × 0.9 缓冲 × 5x,入场 62.5、止损 63.6 + risk = _full_margin_risk_u(23.06, 0.9, 5, "short", 62.5, 63.6) + assert risk is not None + assert 1.5 <= risk <= 2.5