2ce67da8e8
Amp-stats now prices premium from each day's open, and the form switches mutually between straddle and perpetual-options对照. Co-authored-by: Cursor <cursoragent@cursor.com>
574 lines
24 KiB
JavaScript
574 lines
24 KiB
JavaScript
/**
|
|
* 中控振幅统计:OKX ETH/BTC + 买跨/止盈/周末筛选.
|
|
*/
|
|
(function () {
|
|
const page = document.getElementById("page-amp-stats");
|
|
if (!page) return;
|
|
|
|
let lastResult = null;
|
|
let pageNo = 1;
|
|
let bound = false;
|
|
let reframeTimer = null;
|
|
|
|
const el = (id) => document.getElementById(id);
|
|
|
|
async function apiFetch(url, opts) {
|
|
const r = await fetch(url, { credentials: "same-origin", ...(opts || {}) });
|
|
const ct = (r.headers.get("content-type") || "").toLowerCase();
|
|
if (ct.includes("application/json")) {
|
|
const data = await r.json();
|
|
if (!r.ok) throw new Error((data && (data.detail || data.msg)) || r.statusText || "请求失败");
|
|
return data;
|
|
}
|
|
if (!r.ok) throw new Error(r.statusText || "请求失败");
|
|
return r;
|
|
}
|
|
|
|
function esc(s) {
|
|
return String(s ?? "")
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """);
|
|
}
|
|
|
|
function pct(ratio) {
|
|
if (ratio == null || ratio === "") return "—";
|
|
const n = Number(ratio);
|
|
if (!Number.isFinite(n)) return "—";
|
|
return (n * 100).toFixed(1) + "%";
|
|
}
|
|
|
|
function overlayMode() {
|
|
return el("amp-overlay-mode")?.value || "straddle";
|
|
}
|
|
|
|
function syncOverlayMode() {
|
|
const isPerp = overlayMode() === "perp";
|
|
page.querySelectorAll(".amp-overlay-straddle").forEach((n) => n.classList.toggle("hidden", isPerp));
|
|
page.querySelectorAll(".amp-overlay-perp").forEach((n) => n.classList.toggle("hidden", !isPerp));
|
|
el("amp-overlay-straddle-block")?.classList.toggle("hidden", isPerp);
|
|
el("amp-overlay-perp-block")?.classList.toggle("hidden", !isPerp);
|
|
const col = el("amp-col-pnl");
|
|
if (col) col.textContent = isPerp ? "永期盈亏" : "收益";
|
|
}
|
|
|
|
function readPremium() {
|
|
if (overlayMode() !== "straddle") return null;
|
|
const raw = (el("amp-straddle-premium")?.value || "").trim();
|
|
if (!raw) return null;
|
|
const n = Number(raw);
|
|
if (!Number.isFinite(n) || n <= 0) return null;
|
|
return n;
|
|
}
|
|
|
|
function readTakeProfit() {
|
|
if (overlayMode() !== "straddle") return null;
|
|
const raw = (el("amp-take-profit")?.value || "").trim();
|
|
if (!raw) return null;
|
|
const n = Number(raw);
|
|
if (!Number.isFinite(n) || n <= 0) return null;
|
|
return n;
|
|
}
|
|
|
|
function readWeekend() {
|
|
return el("amp-weekend-filter")?.value || "all";
|
|
}
|
|
|
|
function readNum(id) {
|
|
const raw = (el(id)?.value || "").trim();
|
|
if (!raw) return null;
|
|
const n = Number(raw);
|
|
return Number.isFinite(n) ? n : null;
|
|
}
|
|
|
|
function readPerpHedge() {
|
|
if (overlayMode() !== "perp") return null;
|
|
const target = readNum("amp-hedge-target");
|
|
const perpLev = readNum("amp-hedge-perp-lev");
|
|
const optLev = readNum("amp-hedge-opt-lev");
|
|
if (target == null || perpLev == null || optLev == null) return null;
|
|
if (target < 0 || perpLev <= 0 || optLev <= 0) return null;
|
|
return {
|
|
target_profit_u: target,
|
|
perp_leverage: perpLev,
|
|
option_leverage: optLev,
|
|
ratio_perp: readNum("amp-hedge-ratio-perp") || 1,
|
|
ratio_opt: readNum("amp-hedge-ratio-opt") || 2,
|
|
ct_mult: 0.01,
|
|
};
|
|
}
|
|
|
|
function setStatus(msg) {
|
|
const s = el("amp-status");
|
|
if (s) s.textContent = msg || "";
|
|
}
|
|
|
|
function setView(view) {
|
|
const isHist = view === "history";
|
|
el("amp-panel-stats")?.classList.toggle("hidden", isHist);
|
|
el("amp-panel-history")?.classList.toggle("hidden", !isHist);
|
|
page.querySelectorAll(".amp-view-tab").forEach((btn) => {
|
|
const on = btn.getAttribute("data-view") === view;
|
|
btn.classList.toggle("is-active", on);
|
|
btn.setAttribute("aria-selected", on ? "true" : "false");
|
|
});
|
|
if (isHist) void loadHistory();
|
|
}
|
|
|
|
function syncCustomDays() {
|
|
const period = el("amp-period")?.value || "2m";
|
|
const wrap = el("amp-custom-wrap");
|
|
if (wrap) wrap.classList.toggle("hidden", period !== "custom");
|
|
}
|
|
|
|
function fillMetaControls() {
|
|
const hourSel = el("amp-start-hour");
|
|
if (hourSel && !hourSel.options.length) {
|
|
for (let h = 0; h < 24; h++) {
|
|
const opt = document.createElement("option");
|
|
opt.value = String(h);
|
|
opt.textContent = String(h).padStart(2, "0") + ":00";
|
|
if (h === 16) opt.selected = true;
|
|
hourSel.appendChild(opt);
|
|
}
|
|
}
|
|
}
|
|
|
|
function pnlClass(v) {
|
|
const n = Number(v);
|
|
if (!Number.isFinite(n) || n === 0) return "";
|
|
return n > 0 ? "is-pos" : "is-neg";
|
|
}
|
|
|
|
function renderSummary(summary, result) {
|
|
const box = el("amp-summary");
|
|
if (!box) return;
|
|
const s = summary || {};
|
|
syncOverlayMode();
|
|
if (!s.sample_count) {
|
|
box.innerHTML = '<p class="amp-empty">暂无汇总</p>';
|
|
renderStraddle(null);
|
|
renderPerpHedge(null);
|
|
return;
|
|
}
|
|
box.innerHTML =
|
|
`<div class="amp-sum-grid">` +
|
|
`<div><span class="amp-sum-k">样本</span><span class="amp-sum-v">${esc(s.sample_count)}</span></div>` +
|
|
`<div><span class="amp-sum-k">最大振幅</span><span class="amp-sum-v">${esc(s.max_amplitude)} <small>(${esc(s.max_amplitude_day)})</small></span></div>` +
|
|
`<div><span class="amp-sum-k">振幅均值</span><span class="amp-sum-v">${esc(s.avg_amplitude)}</span></div>` +
|
|
`<div><span class="amp-sum-k">振幅中位</span><span class="amp-sum-v">${esc(s.median_amplitude)}</span></div>` +
|
|
`<div><span class="amp-sum-k">开→高最大/均</span><span class="amp-sum-v">${esc(s.max_up_points)} / ${esc(s.avg_up_points)}</span></div>` +
|
|
`<div><span class="amp-sum-k">开→低最大/均</span><span class="amp-sum-v">${esc(s.max_down_points)} / ${esc(s.avg_down_points)}</span></div>` +
|
|
`<div><span class="amp-sum-k">涨/跌窗占比</span><span class="amp-sum-v">${esc(s.up_day_ratio)} / ${esc(s.down_day_ratio)}</span></div>` +
|
|
`<div><span class="amp-sum-k">价源</span><span class="amp-sum-v">${esc(result && result.price_source)}</span></div>` +
|
|
`</div>`;
|
|
if (overlayMode() === "perp") {
|
|
renderStraddle(null);
|
|
renderPerpHedge(s.perp_hedge);
|
|
} else {
|
|
renderPerpHedge(null);
|
|
renderStraddle(s.straddle);
|
|
}
|
|
}
|
|
|
|
function renderStraddle(st) {
|
|
const box = el("amp-straddle");
|
|
if (!box) return;
|
|
if (!st) {
|
|
box.innerHTML = '<p class="amp-empty">填写「买跨·双边权利金」后计算,可看越过天数与买跨点数盈亏</p>';
|
|
return;
|
|
}
|
|
const verdict =
|
|
st.pnl_total == null
|
|
? "—"
|
|
: Number(st.pnl_total) > 0
|
|
? "样本合计盈利"
|
|
: Number(st.pnl_total) < 0
|
|
? "样本合计亏损"
|
|
: "样本合计持平";
|
|
const tpLine =
|
|
st.take_profit != null
|
|
? `<div><span class="amp-sum-k">止盈点 / 触达</span><span class="amp-sum-v">${esc(st.take_profit)} · ${esc(st.tp_hit_days)} 天 · ${esc(pct(st.tp_hit_ratio))}</span></div>`
|
|
: `<div><span class="amp-sum-k">止盈点</span><span class="amp-sum-v">未设(按|涨跌|)</span></div>`;
|
|
box.innerHTML =
|
|
`<div class="amp-sum-grid">` +
|
|
`<div><span class="amp-sum-k">双边权利金</span><span class="amp-sum-v">${esc(st.premium)}</span></div>` +
|
|
tpLine +
|
|
`<div><span class="amp-sum-k">开→高超过权利金</span><span class="amp-sum-v">${esc(st.up_exceed_days)} 天 · ${esc(pct(st.up_exceed_ratio))}</span></div>` +
|
|
`<div><span class="amp-sum-k">开→低超过权利金</span><span class="amp-sum-v">${esc(st.down_exceed_days)} 天 · ${esc(pct(st.down_exceed_ratio))}</span></div>` +
|
|
`<div><span class="amp-sum-k">|涨跌|超过权利金</span><span class="amp-sum-v">${esc(st.abs_change_exceed_days)} 天 · ${esc(pct(st.abs_change_exceed_ratio))}</span></div>` +
|
|
`<div><span class="amp-sum-k">买跨盈亏合计</span><span class="amp-sum-v ${pnlClass(st.pnl_total)}">${esc(st.pnl_total)} <small>(${esc(verdict)})</small></span></div>` +
|
|
`<div><span class="amp-sum-k">日均盈亏</span><span class="amp-sum-v ${pnlClass(st.pnl_avg)}">${esc(st.pnl_avg)}</span></div>` +
|
|
`<div><span class="amp-sum-k">赚钱天数/胜率</span><span class="amp-sum-v">${esc(st.win_days)} · ${esc(pct(st.win_ratio))}</span></div>` +
|
|
`<div><span class="amp-sum-k">单日最大赚/亏</span><span class="amp-sum-v">${esc(st.pnl_max)} / ${esc(st.pnl_min)}</span></div>` +
|
|
`</div>`;
|
|
}
|
|
|
|
function renderPerpHedge(ph) {
|
|
const box = el("amp-perp-hedge");
|
|
if (!box) return;
|
|
if (!ph) {
|
|
box.innerHTML =
|
|
'<p class="amp-empty">填写「目标 / 杠杆」后计算;入场按日开盘;对照所需点数达标与组合盈亏(永续多1币+买期权)</p>';
|
|
return;
|
|
}
|
|
const err =
|
|
ph.points_error
|
|
? `<div><span class="amp-sum-k">推点数提示</span><span class="amp-sum-v">${esc(ph.points_error)}</span></div>`
|
|
: "";
|
|
box.innerHTML =
|
|
`<div class="amp-sum-grid">` +
|
|
`<div><span class="amp-sum-k">入场</span><span class="amp-sum-v">按日开盘 · 推点数中位 ${esc(ph.spot)}</span></div>` +
|
|
`<div><span class="amp-sum-k">比例 / 期权仓</span><span class="amp-sum-v">${esc(ph.ratio_label)} · ${esc(ph.opt_coins)} 币</span></div>` +
|
|
`<div><span class="amp-sum-k">单币/总权利金(中位)</span><span class="amp-sum-v">${esc(ph.prem_per_coin)} / ${esc(ph.premium_total)}</span></div>` +
|
|
`<div><span class="amp-sum-k">A所需点数(永续对)</span><span class="amp-sum-v">${esc(ph.move_a)}</span></div>` +
|
|
`<div><span class="amp-sum-k">A达标</span><span class="amp-sum-v">${esc(ph.hit_a_days)} 天 · ${esc(pct(ph.hit_a_ratio))}</span></div>` +
|
|
`<div><span class="amp-sum-k">B所需点数(组合)</span><span class="amp-sum-v">${esc(ph.move_b)}</span></div>` +
|
|
`<div><span class="amp-sum-k">B达标</span><span class="amp-sum-v">${esc(ph.hit_b_days)} 天 · ${esc(pct(ph.hit_b_ratio))}</span></div>` +
|
|
`<div><span class="amp-sum-k">组合盈亏合计</span><span class="amp-sum-v ${pnlClass(ph.pnl_total)}">${esc(ph.pnl_total)}</span></div>` +
|
|
`<div><span class="amp-sum-k">日均 / 胜率</span><span class="amp-sum-v ${pnlClass(ph.pnl_avg)}">${esc(ph.pnl_avg)} · ${esc(pct(ph.win_ratio))}</span></div>` +
|
|
`<div><span class="amp-sum-k">上涨日盈亏</span><span class="amp-sum-v ${pnlClass(ph.up_pnl_total)}">${esc(ph.up_pnl_total)} <small>(${esc(ph.up_days)}天)</small></span></div>` +
|
|
`<div><span class="amp-sum-k">下跌日盈亏</span><span class="amp-sum-v ${pnlClass(ph.down_pnl_total)}">${esc(ph.down_pnl_total)} <small>(${esc(ph.down_days)}天)</small></span></div>` +
|
|
`<div><span class="amp-sum-k">单日最大赚/亏</span><span class="amp-sum-v">${esc(ph.pnl_max)} / ${esc(ph.pnl_min)}</span></div>` +
|
|
err +
|
|
`</div>`;
|
|
}
|
|
|
|
function dayLabel(r) {
|
|
const day = esc(r.settlement_day);
|
|
if (r.is_weekend && r.weekday_label) {
|
|
return `${day}<span class="amp-wd-tag">${esc(r.weekday_label)}</span>`;
|
|
}
|
|
return day;
|
|
}
|
|
|
|
function renderTable(pagePayload) {
|
|
const body = el("amp-table-body");
|
|
const pager = el("amp-pager");
|
|
if (!body) return;
|
|
const isPerp = overlayMode() === "perp";
|
|
syncOverlayMode();
|
|
const rows = (pagePayload && pagePayload.rows) || [];
|
|
if (!rows.length) {
|
|
body.innerHTML = '<tr><td colspan="11" class="amp-empty">暂无数据</td></tr>';
|
|
} else {
|
|
body.innerHTML = rows
|
|
.map((r) => {
|
|
const pnlVal = isPerp ? r.perp_hedge_pnl : r.profit;
|
|
const pnlCell =
|
|
pnlVal == null || pnlVal === ""
|
|
? "—"
|
|
: `<span class="amp-pnl ${pnlClass(pnlVal)}">${esc(pnlVal)}</span>`;
|
|
const trClass = r.is_weekend ? ' class="amp-row-weekend"' : "";
|
|
return (
|
|
`<tr${trClass}>` +
|
|
`<td>${dayLabel(r)}</td>` +
|
|
`<td>${esc(r.window_start)}</td>` +
|
|
`<td>${esc(r.open)}</td>` +
|
|
`<td>${esc(r.high)}</td>` +
|
|
`<td>${esc(r.low)}</td>` +
|
|
`<td>${esc(r.close)}</td>` +
|
|
`<td>${esc(r.up_points)}</td>` +
|
|
`<td>${esc(r.down_points)}</td>` +
|
|
`<td><strong>${esc(r.amplitude)}</strong></td>` +
|
|
`<td>${esc(r.change)}</td>` +
|
|
`<td>${pnlCell}</td>` +
|
|
`</tr>`
|
|
);
|
|
})
|
|
.join("");
|
|
}
|
|
if (pager && pagePayload) {
|
|
pager.innerHTML =
|
|
`<button type="button" class="ghost" id="amp-page-prev" ${pagePayload.page <= 1 ? "disabled" : ""}>上一页</button>` +
|
|
`<span class="amp-pager-meta">第 ${esc(pagePayload.page)} / ${esc(pagePayload.total_pages)} 页 · 共 ${esc(pagePayload.total)} 天</span>` +
|
|
`<button type="button" class="ghost" id="amp-page-next" ${pagePayload.page >= pagePayload.total_pages ? "disabled" : ""}>下一页</button>`;
|
|
el("amp-page-prev")?.addEventListener("click", () => {
|
|
if (pageNo > 1) {
|
|
pageNo -= 1;
|
|
void reframe(false);
|
|
}
|
|
});
|
|
el("amp-page-next")?.addEventListener("click", () => {
|
|
if (pagePayload.page < pagePayload.total_pages) {
|
|
pageNo += 1;
|
|
void reframe(false);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
function rowsAllFromLast() {
|
|
if (!lastResult) return [];
|
|
if (Array.isArray(lastResult.rows_all) && lastResult.rows_all.length) return lastResult.rows_all;
|
|
return lastResult.rows || [];
|
|
}
|
|
|
|
async function reframe(resetPage) {
|
|
if (!lastResult) {
|
|
renderStraddle(null);
|
|
renderPerpHedge(null);
|
|
return;
|
|
}
|
|
if (resetPage) pageNo = 1;
|
|
const rowsAll = rowsAllFromLast();
|
|
if (!rowsAll.length) return;
|
|
try {
|
|
const data = await apiFetch("/api/amp-stats/reframe", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
rows_all: rowsAll,
|
|
symbol: lastResult.symbol || el("amp-symbol")?.value || "eth",
|
|
start_hour: lastResult.start_hour ?? Number(el("amp-start-hour")?.value || 16),
|
|
period: lastResult.period || el("amp-period")?.value || "2m",
|
|
sample_days: lastResult.sample_days_requested || 60,
|
|
straddle_premium: readPremium(),
|
|
take_profit: readTakeProfit(),
|
|
weekend_filter: readWeekend(),
|
|
perp_hedge: readPerpHedge(),
|
|
price_source: lastResult.price_source || "",
|
|
inst_id: lastResult.inst_id || "",
|
|
page: pageNo,
|
|
page_size: 20,
|
|
}),
|
|
});
|
|
const prevAll = rowsAll;
|
|
lastResult = data.result || lastResult;
|
|
if (!lastResult.rows_all || !lastResult.rows_all.length) lastResult.rows_all = prevAll;
|
|
renderSummary(lastResult.summary, lastResult);
|
|
renderTable(data.page);
|
|
setStatus(`完成 · 样本 ${(lastResult.summary || {}).sample_count || 0}`);
|
|
} catch (e) {
|
|
setStatus(String(e && e.message ? e.message : e));
|
|
}
|
|
}
|
|
|
|
function scheduleReframe() {
|
|
if (!lastResult) return;
|
|
if (reframeTimer) clearTimeout(reframeTimer);
|
|
reframeTimer = setTimeout(() => void reframe(true), 280);
|
|
}
|
|
|
|
async function compute(resetPage) {
|
|
if (resetPage) pageNo = 1;
|
|
const symbol = el("amp-symbol")?.value || "eth";
|
|
const startHour = Number(el("amp-start-hour")?.value || 16);
|
|
const period = el("amp-period")?.value || "2m";
|
|
const customDays = Number(el("amp-custom-days")?.value || 60);
|
|
setStatus("计算中…(长周期会分页拉 OKX,遇限频会自动重试,请稍候)");
|
|
try {
|
|
const data = await apiFetch("/api/amp-stats/compute", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
symbol,
|
|
start_hour: startHour,
|
|
period,
|
|
custom_days: period === "custom" ? customDays : null,
|
|
straddle_premium: readPremium(),
|
|
take_profit: readTakeProfit(),
|
|
weekend_filter: readWeekend(),
|
|
perp_hedge: readPerpHedge(),
|
|
page: pageNo,
|
|
page_size: 20,
|
|
}),
|
|
});
|
|
lastResult = data.result || null;
|
|
renderSummary(lastResult && lastResult.summary, lastResult);
|
|
renderTable(data.page);
|
|
const miss = (lastResult && lastResult.missing_count) || 0;
|
|
setStatus(
|
|
miss
|
|
? `完成 · 样本 ${(lastResult.summary || {}).sample_count || 0} · 缺 ${miss} 天`
|
|
: `完成 · 样本 ${(lastResult.summary || {}).sample_count || 0}`
|
|
);
|
|
} catch (e) {
|
|
setStatus(String(e && e.message ? e.message : e));
|
|
}
|
|
}
|
|
|
|
async function saveHistory() {
|
|
if (!lastResult) {
|
|
setStatus("请先计算");
|
|
return;
|
|
}
|
|
try {
|
|
await apiFetch("/api/amp-stats/history", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ result: lastResult }),
|
|
});
|
|
setStatus("已保存到历史");
|
|
} catch (e) {
|
|
setStatus(String(e && e.message ? e.message : e));
|
|
}
|
|
}
|
|
|
|
function appendHedgeQuery(q) {
|
|
const h = readPerpHedge();
|
|
if (!h) return;
|
|
q.set("hedge_target", String(h.target_profit_u));
|
|
q.set("hedge_perp_lev", String(h.perp_leverage));
|
|
q.set("hedge_opt_lev", String(h.option_leverage));
|
|
q.set("hedge_ratio_perp", String(h.ratio_perp));
|
|
q.set("hedge_ratio_opt", String(h.ratio_opt));
|
|
q.set("hedge_ct_mult", String(h.ct_mult || 0.01));
|
|
}
|
|
|
|
function downloadCurrent() {
|
|
if (!lastResult) {
|
|
setStatus("请先计算");
|
|
return;
|
|
}
|
|
const symbol = el("amp-symbol")?.value || "eth";
|
|
const startHour = Number(el("amp-start-hour")?.value || 16);
|
|
const period = el("amp-period")?.value || "2m";
|
|
const customDays = Number(el("amp-custom-days")?.value || 60);
|
|
const prem = readPremium();
|
|
const tp = readTakeProfit();
|
|
const q = new URLSearchParams({
|
|
symbol,
|
|
start_hour: String(startHour),
|
|
period,
|
|
weekend_filter: readWeekend(),
|
|
});
|
|
if (period === "custom") q.set("custom_days", String(customDays));
|
|
if (prem != null) q.set("straddle_premium", String(prem));
|
|
if (tp != null) q.set("take_profit", String(tp));
|
|
appendHedgeQuery(q);
|
|
window.location.href = "/api/amp-stats/export?" + q.toString();
|
|
}
|
|
|
|
async function loadHistory() {
|
|
const box = el("amp-history-list");
|
|
if (!box) return;
|
|
box.innerHTML = '<p class="amp-empty">加载中…</p>';
|
|
try {
|
|
const data = await apiFetch("/api/amp-stats/history?limit=50");
|
|
const items = data.items || [];
|
|
if (!items.length) {
|
|
box.innerHTML = '<p class="amp-empty">暂无历史</p>';
|
|
return;
|
|
}
|
|
box.innerHTML = items
|
|
.map(
|
|
(it) =>
|
|
`<div class="amp-hist-card" data-id="${esc(it.id)}">` +
|
|
`<div class="amp-hist-main">` +
|
|
`<strong>${esc(it.symbol_label || it.symbol)}</strong> · ${esc(String(it.start_hour).padStart(2, "0"))}:00→16:00 · ${esc(it.period)}` +
|
|
`<div class="amp-hist-sub">${esc(it.created_at)} · 样本 ${esc(it.sample_count)} · 最大振幅 ${esc(it.max_amplitude)} (${esc(it.max_amplitude_day)})</div>` +
|
|
`</div>` +
|
|
`<div class="amp-hist-actions">` +
|
|
`<button type="button" class="ghost amp-hist-view">查看</button>` +
|
|
`<button type="button" class="ghost amp-hist-dl">下载</button>` +
|
|
`<button type="button" class="danger amp-hist-del">删除</button>` +
|
|
`</div></div>`
|
|
)
|
|
.join("");
|
|
box.querySelectorAll(".amp-hist-card").forEach((card) => {
|
|
const id = card.getAttribute("data-id");
|
|
card.querySelector(".amp-hist-view")?.addEventListener("click", () => void openHistory(id));
|
|
card.querySelector(".amp-hist-dl")?.addEventListener("click", () => {
|
|
const prem = readPremium();
|
|
const tp = readTakeProfit();
|
|
const q = new URLSearchParams({
|
|
history_id: id,
|
|
weekend_filter: readWeekend(),
|
|
});
|
|
if (prem != null) q.set("straddle_premium", String(prem));
|
|
if (tp != null) q.set("take_profit", String(tp));
|
|
appendHedgeQuery(q);
|
|
window.location.href = "/api/amp-stats/export?" + q.toString();
|
|
});
|
|
card.querySelector(".amp-hist-del")?.addEventListener("click", async () => {
|
|
if (!confirm("删除该历史记录?")) return;
|
|
await apiFetch("/api/amp-stats/history/" + encodeURIComponent(id), { method: "DELETE" });
|
|
void loadHistory();
|
|
});
|
|
});
|
|
} catch (e) {
|
|
box.innerHTML = `<p class="amp-empty">${esc(String(e && e.message ? e.message : e))}</p>`;
|
|
}
|
|
}
|
|
|
|
async function openHistory(id) {
|
|
try {
|
|
const data = await apiFetch("/api/amp-stats/history/" + encodeURIComponent(id));
|
|
lastResult = data.item || null;
|
|
setView("stats");
|
|
if (lastResult) {
|
|
if (el("amp-symbol")) el("amp-symbol").value = lastResult.symbol || "eth";
|
|
if (el("amp-start-hour")) el("amp-start-hour").value = String(lastResult.start_hour ?? 16);
|
|
if (lastResult.straddle_premium != null && el("amp-straddle-premium")) {
|
|
el("amp-straddle-premium").value = String(lastResult.straddle_premium);
|
|
}
|
|
if (lastResult.take_profit != null && el("amp-take-profit")) {
|
|
el("amp-take-profit").value = String(lastResult.take_profit);
|
|
}
|
|
if (lastResult.weekend_filter && el("amp-weekend-filter")) {
|
|
el("amp-weekend-filter").value = lastResult.weekend_filter;
|
|
}
|
|
const h = lastResult.perp_hedge;
|
|
if (h && typeof h === "object") {
|
|
if (el("amp-overlay-mode")) el("amp-overlay-mode").value = "perp";
|
|
if (h.target_profit_u != null && el("amp-hedge-target")) el("amp-hedge-target").value = String(h.target_profit_u);
|
|
if (h.perp_leverage != null && el("amp-hedge-perp-lev")) el("amp-hedge-perp-lev").value = String(h.perp_leverage);
|
|
if (h.option_leverage != null && el("amp-hedge-opt-lev")) el("amp-hedge-opt-lev").value = String(h.option_leverage);
|
|
if (h.ratio_perp != null && el("amp-hedge-ratio-perp")) el("amp-hedge-ratio-perp").value = String(h.ratio_perp);
|
|
if (h.ratio_opt != null && el("amp-hedge-ratio-opt")) el("amp-hedge-ratio-opt").value = String(h.ratio_opt);
|
|
} else if (lastResult.straddle_premium != null && el("amp-overlay-mode")) {
|
|
el("amp-overlay-mode").value = "straddle";
|
|
}
|
|
syncOverlayMode();
|
|
pageNo = 1;
|
|
setStatus("已载入历史 " + id);
|
|
await reframe(true);
|
|
}
|
|
} catch (e) {
|
|
setStatus(String(e && e.message ? e.message : e));
|
|
}
|
|
}
|
|
|
|
function bind() {
|
|
if (bound) return;
|
|
bound = true;
|
|
fillMetaControls();
|
|
page.querySelectorAll(".amp-view-tab").forEach((btn) => {
|
|
btn.addEventListener("click", () => setView(btn.getAttribute("data-view")));
|
|
});
|
|
el("amp-period")?.addEventListener("change", syncCustomDays);
|
|
el("amp-btn-compute")?.addEventListener("click", () => void compute(true));
|
|
el("amp-btn-save")?.addEventListener("click", () => void saveHistory());
|
|
el("amp-btn-download")?.addEventListener("click", downloadCurrent);
|
|
el("amp-overlay-mode")?.addEventListener("change", () => {
|
|
syncOverlayMode();
|
|
void reframe(true);
|
|
});
|
|
el("amp-straddle-premium")?.addEventListener("input", scheduleReframe);
|
|
el("amp-take-profit")?.addEventListener("input", scheduleReframe);
|
|
[
|
|
"amp-hedge-target",
|
|
"amp-hedge-perp-lev",
|
|
"amp-hedge-opt-lev",
|
|
"amp-hedge-ratio-perp",
|
|
"amp-hedge-ratio-opt",
|
|
].forEach((id) => el(id)?.addEventListener("input", scheduleReframe));
|
|
el("amp-weekend-filter")?.addEventListener("change", () => void reframe(true));
|
|
syncCustomDays();
|
|
syncOverlayMode();
|
|
}
|
|
|
|
window.hubAmpStatsPage = {
|
|
init() {
|
|
bind();
|
|
setView("stats");
|
|
setStatus("");
|
|
syncOverlayMode();
|
|
renderStraddle(null);
|
|
renderPerpHedge(null);
|
|
},
|
|
};
|
|
})();
|