Add hub strategy calculator page with trend and roll risk-based sizing.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-23 17:35:06 +08:00
parent 1ba0014fff
commit 253d353206
8 changed files with 887 additions and 2 deletions
+143
View File
@@ -6804,3 +6804,146 @@ body.funds-fullscreen-open {
}
}
/* ── 策略计算器 ── */
.calc-layout {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px;
align-items: start;
}
.calc-card {
padding: 16px 18px;
}
.calc-card h2 {
margin: 0 0 8px;
font-size: 1rem;
color: var(--text);
}
.calc-hint {
margin: 0 0 14px;
font-size: 0.78rem;
color: var(--muted);
line-height: 1.5;
}
.calc-form-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px 12px;
}
.calc-field {
display: flex;
flex-direction: column;
gap: 5px;
font-size: 0.78rem;
color: var(--muted);
}
.calc-field input,
.calc-field select {
background: var(--bg-elevated);
border: 1px solid var(--border);
color: var(--text);
border-radius: 8px;
padding: 8px 10px;
font-size: 0.82rem;
font-family: var(--mono);
}
.calc-actions {
margin-top: 12px;
}
.calc-result {
margin-top: 14px;
padding-top: 12px;
border-top: 1px solid var(--border-soft);
}
.calc-result.hidden {
display: none !important;
}
.calc-summary {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 8px 12px;
margin-bottom: 12px;
}
.calc-summary div {
background: var(--bg-elevated);
border: 1px solid var(--border-soft);
border-radius: 8px;
padding: 8px 10px;
}
.calc-summary span {
display: block;
font-size: 0.72rem;
color: var(--muted);
margin-bottom: 4px;
}
.calc-summary strong {
font-family: var(--mono);
font-size: 0.86rem;
color: var(--text);
}
.calc-pnl-profit {
color: var(--green) !important;
}
.calc-pnl-loss {
color: var(--red) !important;
}
.calc-table-wrap {
overflow: auto;
}
.calc-table {
width: 100%;
border-collapse: collapse;
font-size: 0.78rem;
}
.calc-table th,
.calc-table td {
padding: 7px 8px;
border-bottom: 1px solid var(--border-soft);
text-align: left;
white-space: nowrap;
}
.calc-table th {
color: var(--muted);
font-weight: 600;
}
.calc-error {
color: var(--red);
font-size: 0.82rem;
margin: 0;
}
.calc-empty {
color: var(--muted);
font-size: 0.82rem;
margin: 0;
}
@media (max-width: 960px) {
.calc-layout {
grid-template-columns: 1fr;
}
.calc-form-grid {
grid-template-columns: 1fr;
}
}
+16
View File
@@ -33,6 +33,10 @@
return displayPref("show_nav_ai", true);
}
function showNavCalculatorPref() {
return displayPref("show_nav_calculator", true);
}
function syncNavVisibility(data) {
const d = (data && data.display) || {};
const navFunds = document.getElementById("nav-funds");
@@ -40,11 +44,13 @@
const navPlan = document.getElementById("nav-plan");
const navArchive = document.getElementById("nav-archive");
const navAi = document.getElementById("nav-ai");
const navCalc = document.getElementById("nav-calculator");
if (navFunds) navFunds.classList.toggle("nav-hidden", d.show_nav_funds === false);
if (navDash) navDash.classList.toggle("nav-hidden", d.show_nav_dashboard === false);
if (navPlan) navPlan.classList.toggle("nav-hidden", d.show_nav_plan === false);
if (navArchive) navArchive.classList.toggle("nav-hidden", d.show_nav_archive === false);
if (navAi) navAi.classList.toggle("nav-hidden", d.show_nav_ai === false);
if (navCalc) navCalc.classList.toggle("nav-hidden", d.show_nav_calculator === false);
}
function pageNavAllowed(page) {
@@ -53,6 +59,7 @@
if (page === "plan") return showNavPlanPref();
if (page === "archive") return showNavArchivePref();
if (page === "ai") return showNavAiPref();
if (page === "calculator") return showNavCalculatorPref();
return true;
}
@@ -64,12 +71,14 @@
const planCb = document.getElementById("pref-show-nav-plan");
const archiveCb = document.getElementById("pref-show-nav-archive");
const aiCb = document.getElementById("pref-show-nav-ai");
const calcCb = document.getElementById("pref-show-nav-calculator");
if (pnlCb) pnlCb.checked = d.show_account_pnl !== false;
if (fundsCb) fundsCb.checked = d.show_nav_funds !== false;
if (dashCb) dashCb.checked = d.show_nav_dashboard !== false;
if (planCb) planCb.checked = d.show_nav_plan !== false;
if (archiveCb) archiveCb.checked = d.show_nav_archive !== false;
if (aiCb) aiCb.checked = d.show_nav_ai !== false;
if (calcCb) calcCb.checked = d.show_nav_calculator !== false;
syncNavVisibility(data);
}
@@ -1035,6 +1044,7 @@
if (p.includes("dashboard")) return "dashboard";
if (p.includes("funds")) return "funds";
if (p.includes("plan")) return "plan";
if (p.includes("calculator")) return "calculator";
if (p.includes("market")) return "market";
if (p.includes("/ai")) return "ai";
return "monitor";
@@ -1046,6 +1056,7 @@
if (page === "dashboard") return "page-dashboard";
if (page === "funds") return "page-funds";
if (page === "plan") return "page-plan";
if (page === "calculator") return "page-calculator";
if (page === "market") return "page-market";
if (page === "ai") return "page-ai";
return "page-monitor";
@@ -1091,6 +1102,9 @@
} else if (window.hubPlanPage && window.hubPlanPage.destroy) {
window.hubPlanPage.destroy();
}
if (page === "calculator" && window.hubCalculatorPage) {
window.hubCalculatorPage.init();
}
if (page === "funds" && window.hubFundsPage) {
window.hubFundsPage.init();
} else if (window.hubFundsPage && window.hubFundsPage.destroy) {
@@ -3770,6 +3784,7 @@
const planCb = document.getElementById("pref-show-nav-plan");
const archiveCb = document.getElementById("pref-show-nav-archive");
const aiCb = document.getElementById("pref-show-nav-ai");
const calcCb = document.getElementById("pref-show-nav-calculator");
return {
version: 1,
display: {
@@ -3779,6 +3794,7 @@
show_nav_plan: planCb ? !!planCb.checked : true,
show_nav_archive: archiveCb ? !!archiveCb.checked : true,
show_nav_ai: aiCb ? !!aiCb.checked : true,
show_nav_calculator: calcCb ? !!calcCb.checked : true,
},
exchanges: rows.map((card) => {
const caps = [];
+249
View File
@@ -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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
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 () {},
};
})();
+124 -2
View File
@@ -15,7 +15,7 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" media="print" onload="this.media='all'" />
<noscript><link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" /></noscript>
<link rel="stylesheet" href="/assets/app.css?v=20260614-macro-panel-padding" />
<link rel="stylesheet" href="/assets/app.css?v=20260614-calculator" />
<link rel="stylesheet" href="/assets/account_risk_badge.css?v=3" />
<script src="/assets/account_risk_badge.js?v=3"></script>
<link rel="stylesheet" href="/assets/dashboard.css?v=20260612-dash-monitor-count" />
@@ -51,6 +51,7 @@
<a href="/plan" id="nav-plan">开仓计划</a>
<a href="/monitor" id="nav-monitor">监控区</a>
<a href="/market" id="nav-market">行情区</a>
<a href="/calculator" id="nav-calculator">计算器</a>
<a href="/archive" id="nav-archive">内照明心</a>
<a href="/dashboard" id="nav-dashboard">数据看板</a>
<a href="/ai" id="nav-ai">AI 教练</a>
@@ -695,6 +696,122 @@
</div>
</div>
<div id="page-calculator" class="page hidden">
<div class="page-head">
<h1><span class="head-tag">CAL</span> 策略计算器</h1>
<p class="page-desc">历史行情测算 · 以损定仓 · 价格均为手动输入</p>
</div>
<div class="calc-layout">
<section class="calc-card card">
<h2>趋势回调计算器</h2>
<p class="calc-hint">逻辑与实例策略页一致:首仓 50% + 补仓网格;止损金额 = 资金 × 风险%。</p>
<form id="calc-trend-form" class="calc-form">
<div class="calc-form-grid">
<label class="calc-field">
<span>交易资金 (U)</span>
<input id="calc-trend-capital" type="number" min="0.01" step="any" value="1000" required />
</label>
<label class="calc-field">
<span>风险 %</span>
<input id="calc-trend-risk" type="number" min="0.1" step="0.1" value="5" required />
</label>
<label class="calc-field">
<span>杠杆</span>
<input id="calc-trend-leverage" type="number" min="1" step="1" value="5" required />
</label>
<label class="calc-field">
<span>方向</span>
<select id="calc-trend-direction">
<option value="long">做多</option>
<option value="short">做空</option>
</select>
</label>
<label class="calc-field">
<span>首仓入场价</span>
<input id="calc-trend-entry" type="number" min="0" step="any" placeholder="手动输入" required />
</label>
<label class="calc-field">
<span>止损价</span>
<input id="calc-trend-sl" type="number" min="0" step="any" required />
</label>
<label class="calc-field">
<span id="calc-trend-add-label">补仓上沿价</span>
<input id="calc-trend-add-upper" type="number" min="0" step="any" required />
</label>
<label class="calc-field">
<span>止盈价</span>
<input id="calc-trend-tp" type="number" min="0" step="any" required />
</label>
<label class="calc-field">
<span>补仓档数</span>
<input id="calc-trend-dca-legs" type="number" min="1" max="20" step="1" value="5" />
</label>
<label class="calc-field">
<span>合约乘数</span>
<input id="calc-trend-contract-size" type="number" min="0.0001" step="any" value="1" title="USDT 线性合约默认 1" />
</label>
</div>
<div class="calc-actions">
<button type="submit" class="primary">计算</button>
</div>
</form>
<div id="calc-trend-result" class="calc-result hidden"></div>
</section>
<section class="calc-card card">
<h2>滚仓计算器</h2>
<p class="calc-hint">逻辑与实例滚仓一致:合并持仓打到新止损 ≈ 账户风险;止盈锁定首仓;加仓价手动输入。</p>
<form id="calc-roll-form" class="calc-form">
<div class="calc-form-grid">
<label class="calc-field">
<span>交易资金 (U)</span>
<input id="calc-roll-capital" type="number" min="0.01" step="any" value="1000" required />
</label>
<label class="calc-field">
<span>总风险 %</span>
<input id="calc-roll-risk" type="number" min="0.1" step="0.1" value="5" required />
</label>
<label class="calc-field">
<span>方向</span>
<select id="calc-roll-direction">
<option value="long">做多</option>
<option value="short">做空</option>
</select>
</label>
<label class="calc-field">
<span>现有张数</span>
<input id="calc-roll-qty" type="number" min="0.0001" step="any" required />
</label>
<label class="calc-field">
<span>现有均价</span>
<input id="calc-roll-entry" type="number" min="0" step="any" required />
</label>
<label class="calc-field">
<span>首仓止盈价</span>
<input id="calc-roll-tp" type="number" min="0" step="any" required />
</label>
<label class="calc-field">
<span>加仓价</span>
<input id="calc-roll-add-price" type="number" min="0" step="any" placeholder="手动输入" required />
</label>
<label class="calc-field">
<span>新统一止损</span>
<input id="calc-roll-sl" type="number" min="0" step="any" required />
</label>
<label class="calc-field">
<span>已完成滚仓次数</span>
<input id="calc-roll-legs-done" type="number" min="0" max="3" step="1" value="0" />
</label>
</div>
<div class="calc-actions">
<button type="submit" class="primary">计算</button>
</div>
</form>
<div id="calc-roll-result" class="calc-result hidden"></div>
</section>
</div>
</div>
<div id="page-settings" class="page hidden">
<div class="page-head">
<h1><span class="head-tag">CFG</span> 系统设置</h1>
@@ -735,6 +852,10 @@
<input type="checkbox" id="pref-show-nav-ai" checked />
顶栏显示「AI 教练」
</label>
<label class="chk-label settings-display-chk">
<input type="checkbox" id="pref-show-nav-calculator" checked />
顶栏显示「计算器」
</label>
<p class="settings-display-hint">保存至 hub_settings.json,换浏览器同样生效。关闭导航后对应页面将不可从顶栏进入,直接访问 URL 会跳回监控区。</p>
</div>
<div class="settings-macro-panel card">
@@ -823,11 +944,12 @@
<script src="/assets/chart_draw.js?v=20260609-market-day-split"></script>
<script src="/assets/chart.js?v=20260609-prev-day-lines"></script>
<script src="/assets/plan.js?v=20260614-entry-plan-scheme"></script>
<script src="/assets/calculator.js?v=1"></script>
<script src="/assets/archive.js?v=20260612-archive-ai-chat"></script>
<script src="/assets/funds.js?v=20260609-hub-funds-fold"></script>
<script src="/assets/dashboard.js?v=20260612-dash-monitor-count"></script>
<script src="/assets/ai_review_render.js?v=3"></script>
<script src="/assets/time_close_ui.js?v=2"></script>
<script src="/assets/app.js?v=20260614-nav-feature-toggles"></script>
<script src="/assets/app.js?v=20260614-calculator"></script>
</body>
</html>