Add weekend filter, take-profit, and profit column to amp stats.

Long-straddle effective move uses TP on path hit (>=) else abs change; mark Sat/Sun on settlement days.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-23 02:47:20 +08:00
parent 789ab43dbe
commit b64c742fc9
7 changed files with 512 additions and 105 deletions
+103 -45
View File
@@ -1,5 +1,5 @@
/**
* 中控振幅统计:OKX ETH/BTC 时段点数振幅 + 买跨对照.
* 中控振幅统计:OKX ETH/BTC + 买跨/止盈/周末筛选.
*/
(function () {
const page = document.getElementById("page-amp-stats");
@@ -8,7 +8,7 @@
let lastResult = null;
let pageNo = 1;
let bound = false;
let straddleTimer = null;
let reframeTimer = null;
const el = (id) => document.getElementById(id);
@@ -47,6 +47,18 @@
return n;
}
function readTakeProfit() {
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 setStatus(msg) {
const s = el("amp-status");
if (s) s.textContent = msg || "";
@@ -127,12 +139,17 @@
: 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>` +
`<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>` +
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>` +
@@ -140,6 +157,14 @@
`</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");
@@ -149,10 +174,15 @@
body.innerHTML = '<tr><td colspan="11" class="amp-empty">暂无数据</td></tr>';
} else {
body.innerHTML = rows
.map(
(r) =>
`<tr>` +
`<td>${esc(r.settlement_day)}</td>` +
.map((r) => {
const profit =
r.profit == null || r.profit === ""
? "—"
: `<span class="${pnlClass(r.profit)}">${esc(r.profit)}</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>` +
@@ -162,8 +192,10 @@
`<td>${esc(r.down_points)}</td>` +
`<td><strong>${esc(r.amplitude)}</strong></td>` +
`<td>${esc(r.change)}</td>` +
`<td>${profit}</td>` +
`</tr>`
)
);
})
.join("");
}
if (pager && pagePayload) {
@@ -174,49 +206,66 @@
el("amp-page-prev")?.addEventListener("click", () => {
if (pageNo > 1) {
pageNo -= 1;
void compute(false);
void reframe(false);
}
});
el("amp-page-next")?.addEventListener("click", () => {
if (pagePayload.page < pagePayload.total_pages) {
pageNo += 1;
void compute(false);
void reframe(false);
}
});
}
}
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;
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);
return;
}
if (resetPage) pageNo = 1;
const rowsAll = rowsAllFromLast();
if (!rowsAll.length) return;
try {
const data = await apiFetch("/api/amp-stats/straddle", {
const data = await apiFetch("/api/amp-stats/reframe", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ rows: lastResult.rows, straddle_premium: prem }),
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(),
price_source: lastResult.price_source || "",
inst_id: lastResult.inst_id || "",
page: pageNo,
page_size: 20,
}),
});
const st = data.straddle || null;
if (!lastResult.summary) lastResult.summary = {};
lastResult.summary.straddle = st;
lastResult.straddle_premium = prem;
renderStraddle(st);
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 scheduleStraddleRefresh() {
if (straddleTimer) clearTimeout(straddleTimer);
straddleTimer = setTimeout(() => void applyStraddleOnly(), 280);
function scheduleReframe() {
if (!lastResult) return;
if (reframeTimer) clearTimeout(reframeTimer);
reframeTimer = setTimeout(() => void reframe(true), 280);
}
async function compute(resetPage) {
@@ -225,7 +274,6 @@
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", {
@@ -236,7 +284,9 @@
start_hour: startHour,
period,
custom_days: period === "custom" ? customDays : null,
straddle_premium: straddlePremium,
straddle_premium: readPremium(),
take_profit: readTakeProfit(),
weekend_filter: readWeekend(),
page: pageNo,
page_size: 20,
}),
@@ -282,13 +332,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));
window.location.href = "/api/amp-stats/export?" + q.toString();
}
@@ -323,8 +376,14 @@
card.querySelector(".amp-hist-view")?.addEventListener("click", () => void openHistory(id));
card.querySelector(".amp-hist-dl")?.addEventListener("click", () => {
const prem = readPremium();
let url = "/api/amp-stats/export?history_id=" + encodeURIComponent(id);
const tp = readTakeProfit();
let url =
"/api/amp-stats/export?history_id=" +
encodeURIComponent(id) +
"&weekend_filter=" +
encodeURIComponent(readWeekend());
if (prem != null) url += "&straddle_premium=" + encodeURIComponent(String(prem));
if (tp != null) url += "&take_profit=" + encodeURIComponent(String(tp));
window.location.href = url;
});
card.querySelector(".amp-hist-del")?.addEventListener("click", async () => {
@@ -349,18 +408,15 @@
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,
page_size: 20,
total: (lastResult.rows || []).length,
total_pages: Math.max(1, Math.ceil((lastResult.rows || []).length / 20)),
rows: (lastResult.rows || []).slice(0, 20),
};
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;
}
pageNo = 1;
renderTable(pagePayload);
setStatus("已载入历史 " + id);
void applyStraddleOnly();
await reframe(true);
}
} catch (e) {
setStatus(String(e && e.message ? e.message : e));
@@ -378,7 +434,9 @@
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);
el("amp-straddle-premium")?.addEventListener("input", scheduleReframe);
el("amp-take-profit")?.addEventListener("input", scheduleReframe);
el("amp-weekend-filter")?.addEventListener("change", () => void reframe(true));
syncCustomDays();
}