Add hub strategy calculator page with trend and roll risk-based sizing.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,249 @@
|
||||
/**
|
||||
* 中控策略计算器:趋势回调 / 滚仓历史测算
|
||||
*/
|
||||
(function () {
|
||||
const page = document.getElementById("page-calculator");
|
||||
if (!page) return;
|
||||
|
||||
let inited = false;
|
||||
|
||||
function $(id) {
|
||||
return document.getElementById(id);
|
||||
}
|
||||
|
||||
function esc(s) {
|
||||
return String(s == null ? "" : s)
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """);
|
||||
}
|
||||
|
||||
function num(id) {
|
||||
const el = $(id);
|
||||
if (!el) return null;
|
||||
const n = Number(el.value);
|
||||
return Number.isFinite(n) ? n : null;
|
||||
}
|
||||
|
||||
function fmt(v, digits) {
|
||||
if (v == null || v === "") return "—";
|
||||
const n = Number(v);
|
||||
if (!Number.isFinite(n)) return esc(v);
|
||||
if (digits != null) return n.toFixed(digits);
|
||||
return String(n);
|
||||
}
|
||||
|
||||
function fmtU(v) {
|
||||
if (v == null || v === "") return "—";
|
||||
const n = Number(v);
|
||||
if (!Number.isFinite(n)) return "—";
|
||||
return (n >= 0 ? "+" : "") + n.toFixed(2) + "U";
|
||||
}
|
||||
|
||||
function pnlClass(v) {
|
||||
const n = Number(v);
|
||||
if (!Number.isFinite(n) || n === 0) return "";
|
||||
return n > 0 ? "calc-pnl-profit" : "calc-pnl-loss";
|
||||
}
|
||||
|
||||
function syncTrendAddLabel() {
|
||||
const dir = ($("calc-trend-direction") && $("calc-trend-direction").value) || "long";
|
||||
const lab = $("calc-trend-add-label");
|
||||
if (lab) lab.textContent = dir === "short" ? "补仓下沿价" : "补仓上沿价";
|
||||
}
|
||||
|
||||
function renderTrendTable(rows) {
|
||||
if (!rows || !rows.length) {
|
||||
return '<p class="calc-empty">无档位数据</p>';
|
||||
}
|
||||
let html =
|
||||
'<div class="calc-table-wrap"><table class="calc-table"><thead><tr>' +
|
||||
"<th>档位</th><th>触发价</th><th>张数</th><th>加仓后均价</th><th>止盈盈利</th><th>止损金额</th><th>盈亏比</th>" +
|
||||
"</tr></thead><tbody>";
|
||||
rows.forEach(function (r) {
|
||||
html +=
|
||||
"<tr>" +
|
||||
"<td>" +
|
||||
esc(r.label) +
|
||||
"</td>" +
|
||||
"<td>" +
|
||||
fmt(r.price, 4) +
|
||||
"</td>" +
|
||||
"<td>" +
|
||||
fmt(r.contracts, 4) +
|
||||
"</td>" +
|
||||
"<td>" +
|
||||
fmt(r.avg_entry, 4) +
|
||||
"</td>" +
|
||||
'<td class="' +
|
||||
pnlClass(r.profit_u) +
|
||||
'">' +
|
||||
fmtU(r.profit_u) +
|
||||
"</td>" +
|
||||
"<td>" +
|
||||
fmtU(r.risk_u) +
|
||||
"</td>" +
|
||||
"<td>" +
|
||||
(r.rr != null ? fmt(r.rr, 2) + ":1" : "—") +
|
||||
"</td>" +
|
||||
"</tr>";
|
||||
});
|
||||
html += "</tbody></table></div>";
|
||||
return html;
|
||||
}
|
||||
|
||||
function renderTrendResult(data) {
|
||||
const box = $("calc-trend-result");
|
||||
if (!box) return;
|
||||
box.classList.remove("hidden");
|
||||
box.innerHTML =
|
||||
'<div class="calc-summary">' +
|
||||
"<div><span>计划保证金</span><strong>" +
|
||||
fmt(data.plan_margin_u, 2) +
|
||||
"U</strong></div>" +
|
||||
"<div><span>止损预算</span><strong>" +
|
||||
fmt(data.risk_budget_u, 2) +
|
||||
"U</strong></div>" +
|
||||
"<div><span>总张数</span><strong>" +
|
||||
fmt(data.target_contracts, 4) +
|
||||
"</strong></div>" +
|
||||
"<div><span>首仓张数</span><strong>" +
|
||||
fmt(data.first_contracts, 4) +
|
||||
"</strong></div>" +
|
||||
'<div><span>首仓止盈盈利</span><strong class="' +
|
||||
pnlClass(data.first_profit_u) +
|
||||
'">' +
|
||||
fmtU(data.first_profit_u) +
|
||||
"</strong></div>" +
|
||||
"<div><span>首仓盈亏比</span><strong>" +
|
||||
(data.first_rr != null ? fmt(data.first_rr, 2) + ":1" : "—") +
|
||||
"</strong></div>" +
|
||||
"</div>" +
|
||||
renderTrendTable(data.rows);
|
||||
}
|
||||
|
||||
function renderRollResult(data) {
|
||||
const box = $("calc-roll-result");
|
||||
if (!box) return;
|
||||
box.classList.remove("hidden");
|
||||
box.innerHTML =
|
||||
'<div class="calc-summary">' +
|
||||
"<div><span>风险预算</span><strong>" +
|
||||
fmt(data.risk_budget_u, 2) +
|
||||
"U</strong></div>" +
|
||||
"<div><span>本次加仓张数</span><strong>" +
|
||||
fmt(data.add_contracts, 4) +
|
||||
"</strong></div>" +
|
||||
"<div><span>合并后张数</span><strong>" +
|
||||
fmt(data.qty_after, 4) +
|
||||
"</strong></div>" +
|
||||
"<div><span>合并后均价</span><strong>" +
|
||||
fmt(data.avg_entry_after, 4) +
|
||||
"</strong></div>" +
|
||||
"<div><span>打到新止损亏损</span><strong class=\"calc-pnl-loss\">" +
|
||||
fmtU(-Math.abs(Number(data.loss_at_sl_u) || 0)) +
|
||||
"</strong></div>" +
|
||||
'<div><span>到达首仓止盈盈利</span><strong class="' +
|
||||
pnlClass(data.profit_at_tp_u) +
|
||||
'">' +
|
||||
fmtU(data.profit_at_tp_u) +
|
||||
"</strong></div>" +
|
||||
"<div><span>金额盈亏比</span><strong>" +
|
||||
(data.rr != null ? fmt(data.rr, 2) + ":1" : "—") +
|
||||
"</strong></div>" +
|
||||
"<div><span>下一滚仓序号</span><strong>第 " +
|
||||
esc(data.leg_index_next) +
|
||||
" 次</strong></div>" +
|
||||
"</div>";
|
||||
}
|
||||
|
||||
function showErr(boxId, msg) {
|
||||
const box = $(boxId);
|
||||
if (!box) return;
|
||||
box.classList.remove("hidden");
|
||||
box.innerHTML = '<p class="calc-error">' + esc(msg || "计算失败") + "</p>";
|
||||
}
|
||||
|
||||
async function submitTrend(e) {
|
||||
e.preventDefault();
|
||||
const body = {
|
||||
direction: ($("calc-trend-direction") && $("calc-trend-direction").value) || "long",
|
||||
capital_usdt: num("calc-trend-capital"),
|
||||
risk_percent: num("calc-trend-risk"),
|
||||
leverage: num("calc-trend-leverage"),
|
||||
entry_price: num("calc-trend-entry"),
|
||||
stop_loss: num("calc-trend-sl"),
|
||||
add_upper: num("calc-trend-add-upper"),
|
||||
take_profit: num("calc-trend-tp"),
|
||||
dca_legs: num("calc-trend-dca-legs") || 5,
|
||||
contract_size: num("calc-trend-contract-size") || 1,
|
||||
};
|
||||
try {
|
||||
const r = await fetch("/api/calculator/trend", {
|
||||
method: "POST",
|
||||
credentials: "same-origin",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
const j = await r.json();
|
||||
if (!j.ok) {
|
||||
showErr("calc-trend-result", j.msg || "计算失败");
|
||||
return;
|
||||
}
|
||||
renderTrendResult(j.data);
|
||||
} catch (err) {
|
||||
showErr("calc-trend-result", String(err));
|
||||
}
|
||||
}
|
||||
|
||||
async function submitRoll(e) {
|
||||
e.preventDefault();
|
||||
const body = {
|
||||
direction: ($("calc-roll-direction") && $("calc-roll-direction").value) || "long",
|
||||
capital_usdt: num("calc-roll-capital"),
|
||||
risk_percent: num("calc-roll-risk"),
|
||||
qty_existing: num("calc-roll-qty"),
|
||||
entry_existing: num("calc-roll-entry"),
|
||||
take_profit: num("calc-roll-tp"),
|
||||
add_price: num("calc-roll-add-price"),
|
||||
new_stop_loss: num("calc-roll-sl"),
|
||||
legs_done: num("calc-roll-legs-done") || 0,
|
||||
};
|
||||
try {
|
||||
const r = await fetch("/api/calculator/roll", {
|
||||
method: "POST",
|
||||
credentials: "same-origin",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
const j = await r.json();
|
||||
if (!j.ok) {
|
||||
showErr("calc-roll-result", j.msg || "计算失败");
|
||||
return;
|
||||
}
|
||||
renderRollResult(j.data);
|
||||
} catch (err) {
|
||||
showErr("calc-roll-result", String(err));
|
||||
}
|
||||
}
|
||||
|
||||
function bindOnce() {
|
||||
if (inited) return;
|
||||
inited = true;
|
||||
const trendForm = $("calc-trend-form");
|
||||
const rollForm = $("calc-roll-form");
|
||||
const dirSel = $("calc-trend-direction");
|
||||
if (trendForm) trendForm.addEventListener("submit", submitTrend);
|
||||
if (rollForm) rollForm.addEventListener("submit", submitRoll);
|
||||
if (dirSel) {
|
||||
dirSel.addEventListener("change", syncTrendAddLabel);
|
||||
syncTrendAddLabel();
|
||||
}
|
||||
}
|
||||
|
||||
window.hubCalculatorPage = {
|
||||
init: bindOnce,
|
||||
destroy: function () {},
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user