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