fix: 全仓模式预估风险/盈利按杠杆与可用保证金计算

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-29 00:38:27 +08:00
parent 9cb63c368a
commit e51d7824a7
8 changed files with 147 additions and 15 deletions
+9 -2
View File
@@ -246,7 +246,14 @@
<link rel="stylesheet" href="/static/instance_theme.css?v=18">
</head>
<body data-page="{{ page }}" data-risk-percent="{{ risk_percent }}">
<body
data-page="{{ page }}"
data-risk-percent="{{ risk_percent }}"
data-position-sizing-mode="{{ position_sizing_mode }}"
data-btc-leverage="{{ btc_leverage }}"
data-alt-leverage="{{ alt_leverage }}"
data-full-margin-buffer="{{ full_margin_buffer_ratio }}"
>
{% macro period_stats(title, s) %}
<div class="stats-period-block">
<h3>{{ title }}</h3>
@@ -833,7 +840,7 @@
<script src="/static/time_close_ui.js?v=2"></script>
<script src="/static/ai_review_render.js?v=2"></script>
<script src="/static/form_submit_guard.js?v=2"></script>
<script src="/static/manual_order_rr_preview.js?v=4"></script>
<script src="/static/manual_order_rr_preview.js?v=5"></script>
<script src="/static/strategy_roll.js?v=5"></script>
<script>
const JOURNAL_ENTRY_REASON_OPTIONS = {{ entry_reason_options | tojson }};
+9 -2
View File
@@ -246,7 +246,14 @@
<link rel="stylesheet" href="/static/instance_theme.css?v=18">
</head>
<body data-page="{{ page }}" data-risk-percent="{{ risk_percent }}">
<body
data-page="{{ page }}"
data-risk-percent="{{ risk_percent }}"
data-position-sizing-mode="{{ position_sizing_mode }}"
data-btc-leverage="{{ btc_leverage }}"
data-alt-leverage="{{ alt_leverage }}"
data-full-margin-buffer="{{ full_margin_buffer_ratio }}"
>
{% macro period_stats(title, s) %}
<div class="stats-period-block">
<h3>{{ title }}</h3>
@@ -800,7 +807,7 @@
<script src="/static/time_close_ui.js?v=2"></script>
<script src="/static/ai_review_render.js?v=2"></script>
<script src="/static/form_submit_guard.js?v=2"></script>
<script src="/static/manual_order_rr_preview.js?v=4"></script>
<script src="/static/manual_order_rr_preview.js?v=5"></script>
<script src="/static/strategy_roll.js?v=5"></script>
<script>
const JOURNAL_ENTRY_REASON_OPTIONS = {{ entry_reason_options | tojson }};
+9 -2
View File
@@ -246,7 +246,14 @@
<link rel="stylesheet" href="/static/instance_theme.css?v=18">
</head>
<body data-page="{{ page }}" data-risk-percent="{{ risk_percent }}">
<body
data-page="{{ page }}"
data-risk-percent="{{ risk_percent }}"
data-position-sizing-mode="{{ position_sizing_mode }}"
data-btc-leverage="{{ btc_leverage }}"
data-alt-leverage="{{ alt_leverage }}"
data-full-margin-buffer="{{ full_margin_buffer_ratio }}"
>
{% macro period_stats(title, s) %}
<div class="stats-period-block">
<h3>{{ title }}</h3>
@@ -800,7 +807,7 @@
<script src="/static/time_close_ui.js?v=2"></script>
<script src="/static/ai_review_render.js?v=2"></script>
<script src="/static/form_submit_guard.js?v=2"></script>
<script src="/static/manual_order_rr_preview.js?v=4"></script>
<script src="/static/manual_order_rr_preview.js?v=5"></script>
<script src="/static/strategy_roll.js?v=5"></script>
<script>
const JOURNAL_ENTRY_REASON_OPTIONS = {{ entry_reason_options | tojson }};
+9 -2
View File
@@ -246,7 +246,14 @@
<link rel="stylesheet" href="/static/instance_theme.css?v=18">
</head>
<body data-page="{{ page }}" data-risk-percent="{{ risk_percent }}">
<body
data-page="{{ page }}"
data-risk-percent="{{ risk_percent }}"
data-position-sizing-mode="{{ position_sizing_mode }}"
data-btc-leverage="{{ btc_leverage }}"
data-alt-leverage="{{ alt_leverage }}"
data-full-margin-buffer="{{ full_margin_buffer_ratio }}"
>
{% macro period_stats(title, s) %}
<div class="stats-period-block">
<h3>{{ title }}</h3>
@@ -829,7 +836,7 @@
<script src="/static/time_close_ui.js?v=2"></script>
<script src="/static/ai_review_render.js?v=2"></script>
<script src="/static/form_submit_guard.js?v=2"></script>
<script src="/static/manual_order_rr_preview.js?v=4"></script>
<script src="/static/manual_order_rr_preview.js?v=5"></script>
<script src="/static/strategy_roll.js?v=5"></script>
<script>
const JOURNAL_ENTRY_REASON_OPTIONS = {{ entry_reason_options | tojson }};
+2 -1
View File
@@ -8,7 +8,8 @@
- **百分比模式**:填完币种、方向、止损%、止盈% 后拉快照校验币种,再显示 RR(`止盈% / 止损%`)。
- **固定盈亏比模式**:不显示预估盈亏比(盈亏比由输入框直接指定;仍保留原有「预估止盈」)。
与计仓方式无关:**以损定仓**`POSITION_SIZING_MODE=risk`**全仓杠杆**`full_margin`)均显示同一预估
- **以损定仓**`POSITION_SIZING_MODE=risk`:预估风险 = 当前交易基数 × `risk%`
- **全仓杠杆**`full_margin`):预估风险 = 合约可用 × 缓冲比例 × 杠杆(BTC/ETH 与山寨按 `.env` 配置)× 止损距离比例,与开仓时 `calc_risk_amount_from_plan` 一致。
## 前端实现
+5 -1
View File
@@ -16,6 +16,10 @@
data-embed-shell="1"
data-risk-percent="{{ risk_percent }}"
data-page="{{ initial_tab }}"
data-position-sizing-mode="{{ position_sizing_mode }}"
data-btc-leverage="{{ btc_leverage }}"
data-alt-leverage="{{ alt_leverage }}"
data-full-margin-buffer="{{ full_margin_buffer_ratio }}"
data-balance-refresh-ms="{{ balance_refresh_seconds * 1000 }}"
data-price-refresh-ms="{{ price_refresh_seconds * 1000 }}"
>
@@ -113,7 +117,7 @@
<script src="/static/time_close_ui.js?v=2"></script>
<script src="/static/ai_review_render.js?v=2"></script>
<script src="/static/form_submit_guard.js?v=2"></script>
<script src="/static/manual_order_rr_preview.js?v=4"></script>
<script src="/static/manual_order_rr_preview.js?v=5"></script>
<script src="/static/strategy_roll.js?v=5"></script>
<script src="/static/key_monitor_form.js?v=1"></script>
{% include 'embed_boot_scripts.html' %}
+77 -5
View File
@@ -1,5 +1,7 @@
/**
* 实盘下单填完币种与止盈止损后在表单下方显示预估风险 / 预估盈利 / 预估盈亏比
* 以损定仓风险 = 当前交易基数 × risk%
* 全仓杠杆风险 = 可用保证金×缓冲 × 杠杆 × |SL-入场|/ calc_risk_amount_from_plan
*/
(function (global) {
"use strict";
@@ -36,6 +38,29 @@
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 =
@@ -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);
+27
View File
@@ -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