Add long-straddle premium overlay to hub amp stats.
Configurable bilateral premium with exceed counts/ratios and settlement PnL for buying volatility. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,14 +1,14 @@
|
||||
/**
|
||||
* 中控振幅统计:OKX ETH/BTC 时段点数振幅.
|
||||
* 中控振幅统计:OKX ETH/BTC 时段点数振幅 + 买跨对照.
|
||||
*/
|
||||
(function () {
|
||||
const page = document.getElementById("page-amp-stats");
|
||||
if (!page) return;
|
||||
|
||||
let meta = null;
|
||||
let lastResult = null;
|
||||
let pageNo = 1;
|
||||
let bound = false;
|
||||
let straddleTimer = null;
|
||||
|
||||
const el = (id) => document.getElementById(id);
|
||||
|
||||
@@ -32,6 +32,21 @@
|
||||
.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 readPremium() {
|
||||
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 setStatus(msg) {
|
||||
const s = el("amp-status");
|
||||
if (s) s.textContent = msg || "";
|
||||
@@ -68,12 +83,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
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 || {};
|
||||
if (!s.sample_count) {
|
||||
box.innerHTML = '<p class="amp-empty">暂无汇总</p>';
|
||||
renderStraddle(null);
|
||||
return;
|
||||
}
|
||||
box.innerHTML =
|
||||
@@ -87,6 +109,35 @@
|
||||
`<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>`;
|
||||
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
|
||||
? "样本合计亏损"
|
||||
: "样本合计持平";
|
||||
box.innerHTML =
|
||||
`<div class="amp-sum-grid">` +
|
||||
`<div><span class="amp-sum-k">双边权利金</span><span class="amp-sum-v">${esc(st.premium)}</span></div>` +
|
||||
`<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 renderTable(pagePayload) {
|
||||
@@ -135,12 +186,46 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function applyStraddleOnly() {
|
||||
if (!lastResult || !Array.isArray(lastResult.rows)) {
|
||||
renderStraddle(null);
|
||||
return;
|
||||
}
|
||||
const prem = readPremium();
|
||||
if (prem == null) {
|
||||
if (lastResult.summary) lastResult.summary.straddle = null;
|
||||
lastResult.straddle_premium = null;
|
||||
renderStraddle(null);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const data = await apiFetch("/api/amp-stats/straddle", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ rows: lastResult.rows, straddle_premium: prem }),
|
||||
});
|
||||
const st = data.straddle || null;
|
||||
if (!lastResult.summary) lastResult.summary = {};
|
||||
lastResult.summary.straddle = st;
|
||||
lastResult.straddle_premium = prem;
|
||||
renderStraddle(st);
|
||||
} catch (e) {
|
||||
setStatus(String(e && e.message ? e.message : e));
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleStraddleRefresh() {
|
||||
if (straddleTimer) clearTimeout(straddleTimer);
|
||||
straddleTimer = setTimeout(() => void applyStraddleOnly(), 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);
|
||||
const straddlePremium = readPremium();
|
||||
setStatus("计算中…(首次拉取 OKX K 线可能需数十秒)");
|
||||
try {
|
||||
const data = await apiFetch("/api/amp-stats/compute", {
|
||||
@@ -151,6 +236,7 @@
|
||||
start_hour: startHour,
|
||||
period,
|
||||
custom_days: period === "custom" ? customDays : null,
|
||||
straddle_premium: straddlePremium,
|
||||
page: pageNo,
|
||||
page_size: 20,
|
||||
}),
|
||||
@@ -195,12 +281,14 @@
|
||||
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 q = new URLSearchParams({
|
||||
symbol,
|
||||
start_hour: String(startHour),
|
||||
period,
|
||||
});
|
||||
if (period === "custom") q.set("custom_days", String(customDays));
|
||||
if (prem != null) q.set("straddle_premium", String(prem));
|
||||
window.location.href = "/api/amp-stats/export?" + q.toString();
|
||||
}
|
||||
|
||||
@@ -234,7 +322,10 @@
|
||||
const id = card.getAttribute("data-id");
|
||||
card.querySelector(".amp-hist-view")?.addEventListener("click", () => void openHistory(id));
|
||||
card.querySelector(".amp-hist-dl")?.addEventListener("click", () => {
|
||||
window.location.href = "/api/amp-stats/export?history_id=" + encodeURIComponent(id);
|
||||
const prem = readPremium();
|
||||
let url = "/api/amp-stats/export?history_id=" + encodeURIComponent(id);
|
||||
if (prem != null) url += "&straddle_premium=" + encodeURIComponent(String(prem));
|
||||
window.location.href = url;
|
||||
});
|
||||
card.querySelector(".amp-hist-del")?.addEventListener("click", async () => {
|
||||
if (!confirm("删除该历史记录?")) return;
|
||||
@@ -255,6 +346,9 @@
|
||||
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);
|
||||
}
|
||||
renderSummary(lastResult.summary, lastResult);
|
||||
const pagePayload = {
|
||||
page: 1,
|
||||
@@ -266,6 +360,7 @@
|
||||
pageNo = 1;
|
||||
renderTable(pagePayload);
|
||||
setStatus("已载入历史 " + id);
|
||||
void applyStraddleOnly();
|
||||
}
|
||||
} catch (e) {
|
||||
setStatus(String(e && e.message ? e.message : e));
|
||||
@@ -283,6 +378,7 @@
|
||||
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-straddle-premium")?.addEventListener("input", scheduleStraddleRefresh);
|
||||
syncCustomDays();
|
||||
}
|
||||
|
||||
@@ -291,6 +387,7 @@
|
||||
bind();
|
||||
setView("stats");
|
||||
setStatus("");
|
||||
renderStraddle(null);
|
||||
},
|
||||
};
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user