/** * 中控策略计算器:趋势回调 / 滚仓历史测算 */ (function () { const page = document.getElementById("page-calculator"); if (!page) return; let inited = false; const marketCache = {}; let calculatorExchanges = []; function $(id) { return document.getElementById(id); } function esc(s) { return String(s == null ? "" : s) .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 text(id) { const el = $(id); if (!el) return ""; return String(el.value || "").trim(); } 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 decimalsFromMarket(data) { if (!data || !data.market) return { price: 4, amount: 4 }; return { price: Number(data.market.price_decimals), amount: Number(data.market.amount_decimals), }; } function fmtMarketInfo(market, err) { if (err) { return '' + esc(err) + ""; } if (!market) return "—"; const inst = market.exchange_name ? esc(market.exchange_name) + " · " : ""; const parts = [ inst + "" + esc(market.display_symbol || market.base || "") + " 永续", "合约 " + esc(market.exchange_symbol || ""), "乘数 " + fmt(market.contract_size, 8), "价格精度 " + fmt(market.price_tick != null ? market.price_tick : Math.pow(10, -(market.price_decimals || 0))), "张数精度 " + fmt(Math.pow(10, -(market.amount_decimals || 0))), ]; if (market.min_amount != null) { parts.push("最小张数 " + fmt(market.min_amount, market.amount_decimals)); } return parts.join(" · "); } function applyMarketSteps(prefix, market) { const pxStep = market && market.price_tick != null && Number(market.price_tick) > 0 ? String(market.price_tick) : market && market.price_decimals != null ? String(Math.pow(10, -Number(market.price_decimals))) : "any"; const amtStep = market && market.amount_decimals != null ? String(Math.pow(10, -Number(market.amount_decimals))) : "any"; page.querySelectorAll("#" + prefix + "-form input[type='number']").forEach(function (el) { if (el.classList.contains("calc-roll-leg-add") || el.classList.contains("calc-roll-leg-stop")) { el.step = pxStep; return; } if (el.id === prefix + "-capital" || el.id === prefix + "-risk" || el.id === prefix + "-leverage") { return; } if (el.id === prefix + "-dca-legs" || el.id === prefix + "-legs-done") { return; } el.step = pxStep; }); page.querySelectorAll(".calc-roll-leg-add, .calc-roll-leg-stop").forEach(function (el) { el.step = pxStep; }); void amtStep; } async function refreshMarket(prefix) { const exchangeEl = $(prefix + "-exchange"); const baseEl = $(prefix + "-base"); const infoEl = $(prefix + "-market-info"); if (!exchangeEl || !baseEl || !infoEl) return null; const exchangeId = exchangeEl.value || (calculatorExchanges[0] && calculatorExchanges[0].id) || "0"; const base = text(prefix + "-base") || "ETH"; const cacheKey = exchangeId + ":" + base.toUpperCase(); infoEl.innerHTML = "加载合约信息…"; try { const r = await fetch( "/api/calculator/market?exchange_id=" + encodeURIComponent(exchangeId) + "&base=" + encodeURIComponent(base), { credentials: "same-origin" } ); const j = await r.json(); if (!j.ok) { infoEl.innerHTML = fmtMarketInfo(null, j.msg || "加载失败"); marketCache[prefix] = null; return null; } marketCache[prefix] = j.data; marketCache[cacheKey] = j.data; infoEl.innerHTML = fmtMarketInfo(j.data, null); applyMarketSteps(prefix, j.data); return j.data; } catch (err) { infoEl.innerHTML = fmtMarketInfo(null, String(err)); marketCache[prefix] = null; return null; } } function fillExchangeSelect(selectEl, selectedId) { if (!selectEl) return; selectEl.innerHTML = ""; if (!calculatorExchanges.length) { selectEl.innerHTML = ''; return; } calculatorExchanges.forEach(function (ex) { const opt = document.createElement("option"); opt.value = String(ex.id); opt.textContent = ex.name || ex.key || ex.id; selectEl.appendChild(opt); }); const want = selectedId != null ? String(selectedId) : String(calculatorExchanges[0].id); if ([].some.call(selectEl.options, function (o) { return o.value === want; })) { selectEl.value = want; } } async function loadCalculatorExchanges() { try { const r = await fetch("/api/calculator/exchanges", { credentials: "same-origin" }); const j = await r.json(); calculatorExchanges = (j.ok && j.data) || []; } catch (_err) { calculatorExchanges = []; } fillExchangeSelect($("calc-trend-exchange")); fillExchangeSelect($("calc-roll-exchange")); } function bindMarket(prefix) { const exchangeEl = $(prefix + "-exchange"); const baseEl = $(prefix + "-base"); if (!exchangeEl || !baseEl) return; const run = function () { void refreshMarket(prefix); }; if (!exchangeEl._calcMarketBound) { exchangeEl._calcMarketBound = true; exchangeEl.addEventListener("change", run); } if (!baseEl._calcMarketBound) { baseEl._calcMarketBound = true; baseEl.addEventListener("change", run); baseEl.addEventListener("blur", run); } run(); } 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, dec) { if (!rows || !rows.length) { return '
无档位数据
'; } const px = dec.price != null ? dec.price : 4; const amt = dec.amount != null ? dec.amount : 4; let html = '| 档位 | 触发价 | 张数 | 加仓后均价 | 止盈盈利 | 止损金额 | 盈亏比 | " + "
|---|---|---|---|---|---|---|
| " + esc(r.label) + " | " + "" + fmt(r.price, px) + " | " + "" + fmt(r.contracts, amt) + " | " + "" + fmt(r.avg_entry, px) + " | " + '' + fmtU(r.profit_u) + " | " + "" + fmtU(r.risk_u) + " | " + "" + (r.rr != null ? fmt(r.rr, 2) + ":1" : "—") + " | " + "
| 阶段 | 入场/加仓价 | 统一止损 | 本次张数 | 累计张数 | 均价 | 打到止损总亏 | 止盈盈利 | 盈亏比 | " + "
|---|---|---|---|---|---|---|---|---|
| " + esc(r.label) + tag + " | " + "" + fmt(r.entry_or_add_price, px) + " | " + "" + fmt(r.stop_loss, px) + " | " + "" + fmt(r.add_contracts, amt) + " | " + "" + fmt(r.total_contracts, amt) + " | " + "" + fmt(r.avg_entry, px) + " | " + '' + fmtU(-Math.abs(Number(r.loss_at_sl_u) || 0)) + " | " + '' + fmtU(r.profit_at_tp_u) + " | " + "" + (r.rr != null ? fmt(r.rr, 2) + ":1" : "—") + " | " + "
' + esc(msg || "计算失败") + "
"; } async function submitTrend(e) { e.preventDefault(); const body = { direction: ($("calc-trend-direction") && $("calc-trend-direction").value) || "long", exchange_id: ($("calc-trend-exchange") && $("calc-trend-exchange").value) || "0", base: text("calc-trend-base") || "ETH", 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, }; 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", exchange_id: ($("calc-roll-exchange") && $("calc-roll-exchange").value) || "0", base: text("calc-roll-base") || "ETH", capital_usdt: num("calc-roll-capital"), risk_percent: num("calc-roll-risk"), entry_price: num("calc-roll-entry"), stop_loss: num("calc-roll-sl"), take_profit: num("calc-roll-tp"), add_legs: collectRollLegs(), 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)); } } async function bindOnce() { if (inited) return; inited = true; await loadCalculatorExchanges(); 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(); } bindRollLegsUI(); bindMarket("calc-trend"); bindMarket("calc-roll"); } window.hubCalculatorPage = { init: function () { bindOnce(); }, destroy: function () {}, }; })();