Add hub-only OKX amp stats for ETH/BTC session windows.

Read-only 1H index candles, point amplitude metrics, history save and CSV export; no order-path changes.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-23 02:06:21 +08:00
parent 40be3a5ab7
commit 61e8da1e8b
15 changed files with 1367 additions and 4 deletions
+137
View File
@@ -0,0 +1,137 @@
"""中控振幅统计 API."""
from __future__ import annotations
from typing import Any, Optional
from fastapi import APIRouter, HTTPException, Query
from fastapi.responses import Response
from pydantic import BaseModel, Field
from amp_stats_store import delete_history, get_history, list_history, save_history
from lib.hub.amp_stats_lib import (
build_export_csv,
compute_amp_stats,
export_filename,
rows_page,
)
class ComputeBody(BaseModel):
symbol: str = "eth"
start_hour: int = 16
period: str = "2m"
custom_days: Optional[int] = None
page: int = 1
page_size: int = 20
class SaveBody(BaseModel):
result: dict[str, Any] = Field(default_factory=dict)
def create_amp_stats_router() -> APIRouter:
router = APIRouter(prefix="/api/amp-stats", tags=["amp-stats"])
@router.get("/meta")
def api_meta():
return {
"ok": True,
"exchange": "okx",
"symbols": [
{"key": "eth", "label": "ETH"},
{"key": "btc", "label": "BTC"},
],
"end_hour": 16,
"start_hours": list(range(24)),
"periods": [
{"key": "1m", "label": "1个月"},
{"key": "2m", "label": "2个月"},
{"key": "3m", "label": "3个月"},
{"key": "6m", "label": "半年"},
{"key": "1y", "label": "1年"},
{"key": "custom", "label": "自定义"},
],
"default_period": "2m",
"timeframe": "1H",
"metric_note": "振幅与距离均为点数:振幅=最高-最低=(开→高)+(开→低)",
}
@router.post("/compute")
def api_compute(body: ComputeBody):
try:
result = compute_amp_stats(
symbol=body.symbol,
start_hour=body.start_hour,
period=body.period,
custom_days=body.custom_days,
)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
except Exception as exc:
raise HTTPException(status_code=502, detail=str(exc)) from exc
page = rows_page(result.get("rows") or [], page=body.page, page_size=body.page_size)
return {
"ok": True,
"result": result,
"page": page,
}
@router.get("/history")
def api_history(symbol: str = "", limit: int = 50):
return {"ok": True, "items": list_history(symbol=symbol, limit=limit)}
@router.post("/history")
def api_history_save(body: SaveBody):
payload = body.result if isinstance(body.result, dict) else {}
if not payload.get("rows") and not payload.get("summary"):
raise HTTPException(status_code=400, detail="无可保存的结果")
item = save_history(payload)
return {"ok": True, "item": item}
@router.get("/history/{item_id}")
def api_history_detail(item_id: str):
item = get_history(item_id)
if not item:
raise HTTPException(status_code=404, detail="历史不存在")
return {"ok": True, "item": item}
@router.delete("/history/{item_id}")
def api_history_delete(item_id: str):
if not delete_history(item_id):
raise HTTPException(status_code=404, detail="历史不存在")
return {"ok": True}
@router.get("/export")
def api_export(
history_id: str = Query(default=""),
symbol: str = Query(default="eth"),
start_hour: int = Query(default=16),
period: str = Query(default="2m"),
custom_days: Optional[int] = Query(default=None),
):
if (history_id or "").strip():
item = get_history(history_id.strip())
if not item:
raise HTTPException(status_code=404, detail="历史不存在")
payload = item
else:
try:
payload = compute_amp_stats(
symbol=symbol,
start_hour=start_hour,
period=period,
custom_days=custom_days,
)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
except Exception as exc:
raise HTTPException(status_code=502, detail=str(exc)) from exc
csv_text = build_export_csv(payload)
name = export_filename(payload)
return Response(
content=csv_text.encode("utf-8"),
media_type="text/csv; charset=utf-8",
headers={"Content-Disposition": f'attachment; filename="{name}"'},
)
return router
+122
View File
@@ -0,0 +1,122 @@
"""振幅统计历史作业存储(中控 JSON)."""
from __future__ import annotations
import json
import threading
import uuid
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Optional
_LOCK = threading.Lock()
_STORE_NAME = "amp_stats_history.json"
_MAX_ITEMS = 80
def _store_path() -> Path:
return Path(__file__).resolve().parent / _STORE_NAME
def _now_iso() -> str:
return datetime.now(timezone.utc).astimezone().strftime("%Y-%m-%d %H:%M:%S")
def _load() -> dict[str, Any]:
path = _store_path()
if not path.is_file():
return {"items": []}
try:
data = json.loads(path.read_text(encoding="utf-8"))
except Exception:
return {"items": []}
if not isinstance(data, dict):
return {"items": []}
items = data.get("items")
if not isinstance(items, list):
items = []
return {"items": items}
def _save(data: dict[str, Any]) -> None:
path = _store_path()
path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
def list_history(*, symbol: str = "", limit: int = 50) -> list[dict[str, Any]]:
with _LOCK:
items = list(_load().get("items") or [])
sym = (symbol or "").strip().lower()
if sym:
items = [x for x in items if str(x.get("symbol") or "").lower() == sym]
limit = max(1, min(200, int(limit or 50)))
out = []
for it in items[:limit]:
out.append(
{
"id": it.get("id"),
"created_at": it.get("created_at"),
"symbol": it.get("symbol"),
"symbol_label": it.get("symbol_label"),
"start_hour": it.get("start_hour"),
"end_hour": it.get("end_hour"),
"period": it.get("period"),
"price_source": it.get("price_source"),
"sample_count": (it.get("summary") or {}).get("sample_count"),
"max_amplitude": (it.get("summary") or {}).get("max_amplitude"),
"max_amplitude_day": (it.get("summary") or {}).get("max_amplitude_day"),
}
)
return out
def get_history(item_id: str) -> Optional[dict[str, Any]]:
iid = (item_id or "").strip()
if not iid:
return None
with _LOCK:
for it in _load().get("items") or []:
if str(it.get("id")) == iid:
return dict(it)
return None
def save_history(payload: dict[str, Any]) -> dict[str, Any]:
item = {
"id": uuid.uuid4().hex[:12],
"created_at": _now_iso(),
"exchange": payload.get("exchange"),
"symbol": payload.get("symbol"),
"symbol_label": payload.get("symbol_label"),
"start_hour": payload.get("start_hour"),
"end_hour": payload.get("end_hour"),
"period": payload.get("period"),
"timeframe": payload.get("timeframe"),
"price_source": payload.get("price_source"),
"inst_id": payload.get("inst_id"),
"timezone": payload.get("timezone"),
"summary": payload.get("summary") or {},
"rows": payload.get("rows") or [],
"missing_count": payload.get("missing_count") or 0,
}
with _LOCK:
data = _load()
items = list(data.get("items") or [])
items.insert(0, item)
data["items"] = items[:_MAX_ITEMS]
_save(data)
return item
def delete_history(item_id: str) -> bool:
iid = (item_id or "").strip()
if not iid:
return False
with _LOCK:
data = _load()
items = list(data.get("items") or [])
new_items = [x for x in items if str(x.get("id")) != iid]
if len(new_items) == len(items):
return False
data["items"] = new_items
_save(data)
return True
@@ -8,6 +8,7 @@
| **开仓计划** | 事前写下计划、跟踪进行中、统计历史胜率 |
| **监控区** | **核心操作台**:三所持仓卡片、全平/撤单、关键位与趋势计划摘要 |
| **策略说明** | 执行手册 + 三所策略 playbook + 开仓检查清单(非系统操作手册) |
| **振幅统计** | OKX ETH/BTC 时段点数振幅档案(只读,固定 16:00 收窗) |
| **使用说明** | 本页:中控与实例怎么用 |
| **行情区** | K 线、指标、画线;可从持仓跳转带币种 |
| **计算器** | 趋势回调 / 滚仓张数与盈亏测算(手动填价) |
+4
View File
@@ -998,6 +998,7 @@ def root_redirect():
@app.get("/funds")
@app.get("/ai")
@app.get("/strategy")
@app.get("/amp-stats")
@app.get("/help")
@app.get("/logs")
@app.get("/settings")
@@ -1012,8 +1013,10 @@ def _all_exchanges_for_ai() -> list:
from hub_ai.routes import create_hub_ai_router
from hub_dashboard import build_dashboard_payload, default_trading_day
from amp_stats_routes import create_amp_stats_router
app.include_router(create_hub_ai_router(load_all_exchanges=_all_exchanges_for_ai))
app.include_router(create_amp_stats_router())
async def _run_dashboard_aggregate() -> dict:
@@ -1111,6 +1114,7 @@ class SettingsDisplayBody(BaseModel):
show_nav_ai: bool = True
show_nav_calculator: bool = True
show_nav_strategy: bool = True
show_nav_amp_stats: bool = True
show_nav_help: bool = True
show_nav_logs: bool = True
+1
View File
@@ -30,6 +30,7 @@ DEFAULT_DISPLAY = {
"show_nav_ai": True,
"show_nav_calculator": True,
"show_nav_strategy": True,
"show_nav_amp_stats": True,
"show_nav_help": True,
"show_nav_logs": True,
}
+296
View File
@@ -0,0 +1,296 @@
/**
* 中控振幅统计: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;
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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
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 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>';
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>`;
}
function renderTable(pagePayload) {
const body = el("amp-table-body");
const pager = el("amp-pager");
if (!body) return;
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) =>
`<tr>` +
`<td>${esc(r.settlement_day)}</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>` +
`</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 compute(false);
}
});
el("amp-page-next")?.addEventListener("click", () => {
if (pagePayload.page < pagePayload.total_pages) {
pageNo += 1;
void compute(false);
}
});
}
}
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 K 线可能需数十秒)");
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,
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 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 q = new URLSearchParams({
symbol,
start_hour: String(startHour),
period,
});
if (period === "custom") q.set("custom_days", String(customDays));
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", () => {
window.location.href = "/api/amp-stats/export?history_id=" + encodeURIComponent(id);
});
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);
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),
};
pageNo = 1;
renderTable(pagePayload);
setStatus("已载入历史 " + id);
}
} 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);
syncCustomDays();
}
window.hubAmpStatsPage = {
init() {
bind();
setView("stats");
setStatus("");
},
};
})();
+52
View File
@@ -10939,3 +10939,55 @@ 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; }
}
+16
View File
@@ -45,6 +45,10 @@
return displayPref("show_nav_strategy", true);
}
function showNavAmpStatsPref() {
return displayPref("show_nav_amp_stats", true);
}
function showNavHelpPref() {
return displayPref("show_nav_help", true);
}
@@ -64,6 +68,7 @@
["nav-ai", "m-tab-ai", d.show_nav_ai === false],
["nav-calculator", "m-tab-calculator", d.show_nav_calculator === false],
["nav-strategy", "m-nav-strategy", d.show_nav_strategy === false],
["nav-amp-stats", "m-nav-amp-stats", d.show_nav_amp_stats === false],
["nav-help", "m-nav-help", d.show_nav_help === false],
["nav-logs", "m-nav-logs", d.show_nav_logs === false],
];
@@ -136,6 +141,7 @@
if (page === "ai") return showNavAiPref();
if (page === "calculator") return showNavCalculatorPref();
if (page === "strategy") return showNavStrategyPref();
if (page === "amp-stats") return showNavAmpStatsPref();
if (page === "help") return showNavHelpPref();
if (page === "logs") return showNavLogsPref();
return true;
@@ -152,6 +158,7 @@
const aiCb = document.getElementById("pref-show-nav-ai");
const calcCb = document.getElementById("pref-show-nav-calculator");
const strategyCb = document.getElementById("pref-show-nav-strategy");
const ampCb = document.getElementById("pref-show-nav-amp-stats");
const helpCb = document.getElementById("pref-show-nav-help");
const logsCb = document.getElementById("pref-show-nav-logs");
if (pnlCb) pnlCb.checked = d.show_account_pnl !== false;
@@ -163,6 +170,7 @@
if (aiCb) aiCb.checked = d.show_nav_ai !== false;
if (calcCb) calcCb.checked = d.show_nav_calculator !== false;
if (strategyCb) strategyCb.checked = d.show_nav_strategy !== false;
if (ampCb) ampCb.checked = d.show_nav_amp_stats !== false;
if (helpCb) helpCb.checked = d.show_nav_help !== false;
if (logsCb) logsCb.checked = d.show_nav_logs !== false;
syncNavVisibility(data);
@@ -1278,6 +1286,7 @@
if (p.includes("plan")) return "plan";
if (p.includes("calculator")) return "calculator";
if (p.includes("help")) return "help";
if (p.includes("amp-stats")) return "amp-stats";
if (p.includes("strategy")) return "strategy";
if (p.includes("logs")) return "logs";
if (p.includes("market")) return "market";
@@ -1295,6 +1304,7 @@
if (page === "calculator") return "page-calculator";
if (page === "help") return "page-help";
if (page === "strategy") return "page-strategy";
if (page === "amp-stats") return "page-amp-stats";
if (page === "logs") return "page-logs";
if (page === "market") return "page-market";
if (page === "ai") return "page-ai";
@@ -1329,6 +1339,7 @@
document.body.classList.toggle("hub-page-quotes", page === "quotes");
document.body.classList.toggle("hub-page-plan", page === "plan");
document.body.classList.toggle("hub-page-strategy", page === "strategy");
document.body.classList.toggle("hub-page-amp-stats", page === "amp-stats");
document.body.classList.toggle("hub-page-logs", page === "logs");
document.body.classList.toggle("hub-page-help", page === "help");
syncHubPhoneShellClass();
@@ -1373,6 +1384,9 @@
} else if (window.hubStrategyPage && window.hubStrategyPage.destroy) {
window.hubStrategyPage.destroy();
}
if (page === "amp-stats" && window.hubAmpStatsPage) {
window.hubAmpStatsPage.init();
}
if (page === "help" && window.hubHelpPage) {
window.hubHelpPage.init();
} else if (window.hubHelpPage && window.hubHelpPage.destroy) {
@@ -5052,6 +5066,7 @@
const aiCb = document.getElementById("pref-show-nav-ai");
const calcCb = document.getElementById("pref-show-nav-calculator");
const strategyCb = document.getElementById("pref-show-nav-strategy");
const ampCb = document.getElementById("pref-show-nav-amp-stats");
const helpCb = document.getElementById("pref-show-nav-help");
const logsCb = document.getElementById("pref-show-nav-logs");
const supEnabled = document.getElementById("supervisor-enabled");
@@ -5075,6 +5090,7 @@
show_nav_ai: aiCb ? !!aiCb.checked : true,
show_nav_calculator: calcCb ? !!calcCb.checked : true,
show_nav_strategy: strategyCb ? !!strategyCb.checked : true,
show_nav_amp_stats: ampCb ? !!ampCb.checked : true,
show_nav_help: helpCb ? !!helpCb.checked : true,
show_nav_logs: logsCb ? !!logsCb.checked : true,
},
+87 -2
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=20260722-monitor-m-stats" />
<link rel="stylesheet" href="/assets/app.css?v=20260723-amp-stats" />
<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>
@@ -53,6 +53,7 @@
<a href="/plan" id="nav-plan">开仓计划</a>
<a href="/monitor" id="nav-monitor">监控区</a>
<a href="/strategy" id="nav-strategy">策略说明</a>
<a href="/amp-stats" id="nav-amp-stats">振幅统计</a>
<a href="/help" id="nav-help">使用说明</a>
<a href="/market" id="nav-market">行情区</a>
<a href="/calculator" id="nav-calculator">计算器</a>
@@ -1003,6 +1004,84 @@
</div>
</div>
<div id="page-amp-stats" class="page hidden">
<div class="page-head">
<div>
<h1><span class="head-tag">AMP</span> 振幅统计</h1>
<p class="page-desc">OKX 指数 · 整点起点 → 固定 16:00 · 点数振幅档案(只读)</p>
</div>
</div>
<div class="amp-view-tabs" role="tablist" aria-label="振幅视图">
<button type="button" class="amp-view-tab is-active" data-view="stats" role="tab" aria-selected="true">统计</button>
<button type="button" class="amp-view-tab" data-view="history" role="tab" aria-selected="false">历史</button>
</div>
<section id="amp-panel-stats" class="card amp-panel">
<div class="amp-form">
<label class="amp-field">
<span>标的</span>
<select id="amp-symbol">
<option value="eth" selected>ETH</option>
<option value="btc">BTC</option>
</select>
</label>
<label class="amp-field">
<span>数据源</span>
<input type="text" value="OKX" disabled />
</label>
<label class="amp-field">
<span>起点整点</span>
<select id="amp-start-hour"></select>
</label>
<label class="amp-field">
<span>终点</span>
<input type="text" value="16:00" disabled />
</label>
<label class="amp-field">
<span>周期</span>
<select id="amp-period">
<option value="1m">1个月</option>
<option value="2m" selected>2个月</option>
<option value="3m">3个月</option>
<option value="6m">半年</option>
<option value="1y">1年</option>
<option value="custom">自定义</option>
</select>
</label>
<label class="amp-field hidden" id="amp-custom-wrap">
<span>自定义天数</span>
<input id="amp-custom-days" type="number" min="7" max="400" value="60" />
</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>
<button type="button" id="amp-btn-download" class="ghost">下载 CSV</button>
</div>
</div>
<p id="amp-status" class="toolbar-meta amp-status"></p>
<p class="amp-hint">口径:开→高=最高−开盘;开→低=开盘−最低;振幅=最高−最低(均为点数).未到 16:00 的当日不入样.</p>
<h3 class="amp-block-title">汇总</h3>
<div id="amp-summary" class="amp-summary"></div>
<h3 class="amp-block-title">日表明细</h3>
<div class="amp-table-wrap">
<table class="amp-table">
<thead>
<tr>
<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>
</tbody>
</table>
</div>
<div id="amp-pager" class="amp-pager"></div>
</section>
<section id="amp-panel-history" class="card amp-panel hidden">
<div id="amp-history-list" class="amp-history-list"></div>
</section>
</div>
<div id="page-help" class="page hidden">
<div class="page-head">
<div>
@@ -1149,6 +1228,10 @@
<input type="checkbox" id="pref-show-nav-strategy" checked />
顶栏显示「策略说明」
</label>
<label class="chk-label settings-display-chk">
<input type="checkbox" id="pref-show-nav-amp-stats" checked />
顶栏显示「振幅统计」
</label>
<label class="chk-label settings-display-chk">
<input type="checkbox" id="pref-show-nav-help" checked />
顶栏显示「使用说明」
@@ -1343,6 +1426,7 @@
<a href="/quotes" id="m-nav-quotes">语录</a>
<a href="/dashboard" id="m-nav-dashboard">数据看板</a>
<a href="/strategy" id="m-nav-strategy">策略说明</a>
<a href="/amp-stats" id="m-nav-amp-stats">振幅统计</a>
<a href="/help" id="m-nav-help">使用说明</a>
<a href="/logs" id="m-nav-logs">系统日志</a>
<a href="/settings" id="m-nav-settings">系统设置</a>
@@ -1406,6 +1490,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/help.js?v=1"></script>
<script src="/assets/logs.js?v=1"></script>
<script src="/assets/ai_review_render.js?v=3"></script>
@@ -1413,6 +1498,6 @@
<script src="/assets/options_expiry_countdown.js?v=1"></script>
<script src="/assets/options_position_cards.js?v=2"></script>
<script src="/assets/backup.js?v=1"></script>
<script src="/assets/app.js?v=20260720-dash-back"></script>
<script src="/assets/app.js?v=20260723-amp-stats"></script>
</body>
</html>