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:
dekun
2026-07-23 02:33:23 +08:00
parent 61e8da1e8b
commit 789ab43dbe
7 changed files with 347 additions and 61 deletions
+100 -3
View File
@@ -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, "&quot;");
}
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);
},
};
})();
+54 -51
View File
@@ -10939,55 +10939,58 @@ html[data-theme="light"] .hub-logs-card-hint {
min-height: 360px;
}
}
/* —— 振幅统计 —— */
.amp-view-tabs { display: flex; gap: 8px; margin: 0 0 12px; }
.amp-view-tab {
min-height: 34px; padding: 6px 14px; border: 1px solid var(--border-soft);
border-radius: 8px; background: transparent; color: var(--muted); cursor: pointer;
}
.amp-view-tab.is-active { color: var(--text); border-color: var(--accent); background: rgba(0, 212, 255, 0.08); }
.amp-panel { padding: 14px 16px 18px; }
.amp-form {
display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 10px 12px; align-items: end; margin-bottom: 8px;
}
.amp-field { display: flex; flex-direction: column; gap: 4px; font-size: 12px; color: var(--muted); }
.amp-field select, .amp-field input {
min-height: 34px; padding: 6px 8px; border-radius: 8px;
border: 1px solid var(--border-soft); background: var(--panel-solid); color: var(--text);
}
.amp-actions { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
.amp-hint { font-size: 12px; color: var(--muted); margin: 4px 0 12px; }
.amp-status { margin: 0 0 8px; }
.amp-block-title { font-size: 14px; margin: 14px 0 8px; }
.amp-sum-grid {
display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 8px;
}
.amp-sum-grid > div {
border: 1px solid var(--border-soft); border-radius: 8px; padding: 8px 10px;
display: flex; flex-direction: column; gap: 2px;
}
.amp-sum-k { font-size: 11px; color: var(--muted); }
.amp-sum-v { font-size: 14px; font-weight: 600; color: var(--text); }
.amp-table-wrap { overflow-x: auto; }
.amp-table { width: 100%; border-collapse: collapse; font-size: 12px; }
.amp-table th, .amp-table td {
border-bottom: 1px solid var(--border-soft); padding: 7px 8px; text-align: right; white-space: nowrap;
}
.amp-table th:first-child, .amp-table td:first-child,
.amp-table th:nth-child(2), .amp-table td:nth-child(2) { text-align: left; }
.amp-pager { display: flex; align-items: center; gap: 10px; margin-top: 10px; }
.amp-pager-meta { font-size: 12px; color: var(--muted); }
.amp-empty { color: var(--muted); text-align: center; padding: 16px; }
.amp-history-list { display: flex; flex-direction: column; gap: 10px; }
.amp-hist-card {
display: flex; justify-content: space-between; gap: 12px; flex-wrap: wrap;
border: 1px solid var(--border-soft); border-radius: 10px; padding: 10px 12px;
}
.amp-hist-sub { font-size: 12px; color: var(--muted); margin-top: 4px; }
.amp-hist-actions { display: flex; gap: 6px; align-items: center; }
@media (max-width: 720px) {
.amp-form { grid-template-columns: 1fr 1fr; }
.amp-actions { grid-column: 1 / -1; }
/* —— 振幅统计 —— */
.amp-view-tabs { display: flex; gap: 8px; margin: 0 0 12px; }
.amp-view-tab {
min-height: 34px; padding: 6px 14px; border: 1px solid var(--border-soft);
border-radius: 8px; background: transparent; color: var(--muted); cursor: pointer;
}
.amp-view-tab.is-active { color: var(--text); border-color: var(--accent); background: rgba(0, 212, 255, 0.08); }
.amp-panel { padding: 14px 16px 18px; }
.amp-form {
display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 10px 12px; align-items: end; margin-bottom: 8px;
}
.amp-field { display: flex; flex-direction: column; gap: 4px; font-size: 12px; color: var(--muted); }
.amp-field select, .amp-field input {
min-height: 34px; padding: 6px 8px; border-radius: 8px;
border: 1px solid var(--border-soft); background: var(--panel-solid); color: var(--text);
}
.amp-actions { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
.amp-hint { font-size: 12px; color: var(--muted); margin: 4px 0 12px; }
.amp-status { margin: 0 0 8px; }
.amp-block-title { font-size: 14px; margin: 14px 0 8px; }
.amp-sum-grid {
display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 8px;
}
.amp-sum-grid > div {
border: 1px solid var(--border-soft); border-radius: 8px; padding: 8px 10px;
display: flex; flex-direction: column; gap: 2px;
}
.amp-sum-k { font-size: 11px; color: var(--muted); }
.amp-sum-v { font-size: 14px; font-weight: 600; color: var(--text); }
.amp-sum-v.is-pos { color: var(--green); }
.amp-sum-v.is-neg { color: var(--red); }
.amp-straddle { margin-bottom: 4px; }
.amp-table-wrap { overflow-x: auto; }
.amp-table { width: 100%; border-collapse: collapse; font-size: 12px; }
.amp-table th, .amp-table td {
border-bottom: 1px solid var(--border-soft); padding: 7px 8px; text-align: right; white-space: nowrap;
}
.amp-table th:first-child, .amp-table td:first-child,
.amp-table th:nth-child(2), .amp-table td:nth-child(2) { text-align: left; }
.amp-pager { display: flex; align-items: center; gap: 10px; margin-top: 10px; }
.amp-pager-meta { font-size: 12px; color: var(--muted); }
.amp-empty { color: var(--muted); text-align: center; padding: 16px; }
.amp-history-list { display: flex; flex-direction: column; gap: 10px; }
.amp-hist-card {
display: flex; justify-content: space-between; gap: 12px; flex-wrap: wrap;
border: 1px solid var(--border-soft); border-radius: 10px; padding: 10px 12px;
}
.amp-hist-sub { font-size: 12px; color: var(--muted); margin-top: 4px; }
.amp-hist-actions { display: flex; gap: 6px; align-items: center; }
@media (max-width: 720px) {
.amp-form { grid-template-columns: 1fr 1fr; }
.amp-actions { grid-column: 1 / -1; }
}
+9 -3
View File
@@ -16,7 +16,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=20260723-amp-stats" />
<link rel="stylesheet" href="/assets/app.css?v=20260723-amp-straddle" />
<link rel="stylesheet" href="/assets/trade_stats_calendar.css?v=4" />
<link rel="stylesheet" href="/assets/account_risk_badge.css?v=4" />
<script src="/assets/account_risk_badge.js?v=4"></script>
@@ -1051,6 +1051,10 @@
<span>自定义天数</span>
<input id="amp-custom-days" type="number" min="7" max="400" value="60" />
</label>
<label class="amp-field">
<span>买跨·双边权利金(点)</span>
<input id="amp-straddle-premium" type="number" min="0" step="any" placeholder="如 30" />
</label>
<div class="amp-actions">
<button type="button" id="amp-btn-compute" class="primary">计算</button>
<button type="button" id="amp-btn-save" class="ghost">保存到历史</button>
@@ -1058,9 +1062,11 @@
</div>
</div>
<p id="amp-status" class="toolbar-meta amp-status"></p>
<p class="amp-hint">口径:开→高=最高−开盘;开→低=开盘−最低;振幅=最高−最低(均为点数).未到 16:00 的当日不入样.</p>
<p class="amp-hint">口径:开→高=最高−开盘;开→低=开盘−最低;振幅=最高−最低(均为点数).未到 16:00 的当日不入样.买跨对照:越过用严格 &gt;;盈亏=|收−开|−双边权利金.</p>
<h3 class="amp-block-title">汇总</h3>
<div id="amp-summary" class="amp-summary"></div>
<h3 class="amp-block-title">买跨对照</h3>
<div id="amp-straddle" class="amp-summary amp-straddle"></div>
<h3 class="amp-block-title">日表明细</h3>
<div class="amp-table-wrap">
<table class="amp-table">
@@ -1490,7 +1496,7 @@
<script src="/assets/funds.js?v=20260717-funds-scroll-fix"></script>
<script src="/assets/dashboard.js?v=20260720-dash-sl-tp"></script>
<script src="/assets/strategy.js?v=9"></script>
<script src="/assets/amp_stats.js?v=1"></script>
<script src="/assets/amp_stats.js?v=2"></script>
<script src="/assets/help.js?v=1"></script>
<script src="/assets/logs.js?v=1"></script>
<script src="/assets/ai_review_render.js?v=3"></script>