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:
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -10973,6 +10973,12 @@ html[data-theme="light"] .hub-logs-card-hint {
|
||||
.amp-sum-v.is-pos { color: var(--green); }
|
||||
.amp-sum-v.is-neg { color: var(--red); }
|
||||
.amp-straddle { margin-bottom: 4px; }
|
||||
.amp-table tr.amp-row-weekend td { background: rgba(255, 180, 60, 0.08); }
|
||||
.amp-wd-tag {
|
||||
display: inline-block; margin-left: 6px; padding: 1px 6px; border-radius: 4px;
|
||||
font-size: 11px; font-weight: 600; color: #f0c14b;
|
||||
border: 1px solid rgba(240, 193, 75, 0.45);
|
||||
}
|
||||
.amp-table-wrap { overflow-x: auto; }
|
||||
.amp-table { width: 100%; border-collapse: collapse; font-size: 12px; }
|
||||
.amp-table th, .amp-table td {
|
||||
|
||||
@@ -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-straddle" />
|
||||
<link rel="stylesheet" href="/assets/app.css?v=20260723-amp-tp" />
|
||||
<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,10 +1051,22 @@
|
||||
<span>自定义天数</span>
|
||||
<input id="amp-custom-days" type="number" min="7" max="400" value="60" />
|
||||
</label>
|
||||
<label class="amp-field">
|
||||
<span>周末</span>
|
||||
<select id="amp-weekend-filter">
|
||||
<option value="all" selected>全部</option>
|
||||
<option value="exclude">排除周末</option>
|
||||
<option value="only">仅周末</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="amp-field">
|
||||
<span>买跨·双边权利金(点)</span>
|
||||
<input id="amp-straddle-premium" type="number" min="0" step="any" placeholder="如 30" />
|
||||
</label>
|
||||
<label class="amp-field">
|
||||
<span>止盈点(点)</span>
|
||||
<input id="amp-take-profit" type="number" min="0" step="any" placeholder="空=按涨跌" />
|
||||
</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>
|
||||
@@ -1062,7 +1074,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<p id="amp-status" class="toolbar-meta amp-status"></p>
|
||||
<p class="amp-hint">口径:开→高=最高−开盘;开→低=开盘−最低;振幅=最高−最低(均为点数).未到 16:00 的当日不入样.买跨对照:越过用严格 >;盈亏=|收−开|−双边权利金.</p>
|
||||
<p class="amp-hint">口径:开→高=最高−开盘;开→低=开盘−最低;振幅=最高−最低.买跨收益=有效波动−权利金;止盈≥触达则有效波动=止盈点,否则用|涨跌|.周末按结算日标注/筛选.</p>
|
||||
<h3 class="amp-block-title">汇总</h3>
|
||||
<div id="amp-summary" class="amp-summary"></div>
|
||||
<h3 class="amp-block-title">买跨对照</h3>
|
||||
@@ -1073,11 +1085,11 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th>结算日</th><th>窗起点</th><th>开</th><th>高</th><th>低</th><th>收</th>
|
||||
<th>开→高</th><th>开→低</th><th>振幅</th><th>涨跌</th>
|
||||
<th>开→高</th><th>开→低</th><th>振幅</th><th>涨跌</th><th>收益</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="amp-table-body">
|
||||
<tr><td colspan="10" class="amp-empty">点击「计算」加载</td></tr>
|
||||
<tr><td colspan="11" class="amp-empty">点击「计算」加载</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -1496,7 +1508,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=2"></script>
|
||||
<script src="/assets/amp_stats.js?v=3"></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>
|
||||
|
||||
Reference in New Issue
Block a user