fix: 全仓模式预估风险/盈利按杠杆与可用保证金计算
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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 }};
|
||||
|
||||
@@ -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 }};
|
||||
|
||||
@@ -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 }};
|
||||
|
||||
@@ -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 }};
|
||||
|
||||
@@ -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` 一致。
|
||||
|
||||
## 前端实现
|
||||
|
||||
|
||||
@@ -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' %}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user