feat(hub): add period date range and trade stats to inner-light-mind

Support today/week/month/custom range selection with sick count, PnL, and per-exchange breakdown; update docs.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-11 18:09:39 +08:00
parent 5f79a62b13
commit 7b0b8996fe
7 changed files with 360 additions and 62 deletions
+108 -9
View File
@@ -9,7 +9,11 @@
const elFilterProfit = document.getElementById("archive-filter-profit");
const elFilterLoss = document.getElementById("archive-filter-loss");
const elFilterSick = document.getElementById("archive-filter-sick");
const elPeriodTabs = document.getElementById("archive-period-tabs");
const elTradingDay = document.getElementById("archive-trading-day");
const elPeriodRangeWrap = document.getElementById("archive-period-range-wrap");
const elDateFrom = document.getElementById("archive-date-from");
const elDateTo = document.getElementById("archive-date-to");
const elSearch = document.getElementById("archive-search");
const elBtnChartToggle = document.getElementById("archive-btn-chart-toggle");
const elBtnRefresh = document.getElementById("archive-btn-refresh");
@@ -45,6 +49,10 @@
let quotes = [];
let dailyTrades = [];
let dailyStats = { open_count: 0, by_exchange: {} };
let periodMode = "today";
let periodLabel = "";
let dateFrom = "";
let dateTo = "";
let tradingDay = "";
let selected = null;
let trades = [];
@@ -292,9 +300,35 @@
return r;
}
function syncPeriodUI() {
if (elPeriodTabs) {
elPeriodTabs.querySelectorAll(".archive-period-btn").forEach(function (btn) {
btn.classList.toggle("is-active", btn.getAttribute("data-period") === periodMode);
});
}
if (elTradingDay) {
elTradingDay.classList.toggle("hidden", periodMode !== "today");
}
if (elPeriodRangeWrap) {
elPeriodRangeWrap.classList.toggle("hidden", periodMode !== "range");
}
}
function setPeriodMode(mode) {
periodMode = mode || "today";
syncPeriodUI();
}
function queryDailyParams() {
const q = new URLSearchParams();
if (elTradingDay && elTradingDay.value) q.set("trading_day", elTradingDay.value);
q.set("period", periodMode);
if (periodMode === "today" && elTradingDay && elTradingDay.value) {
q.set("trading_day", elTradingDay.value);
}
if (periodMode === "range") {
if (elDateFrom && elDateFrom.value) q.set("date_from", elDateFrom.value);
if (elDateTo && elDateTo.value) q.set("date_to", elDateTo.value);
}
const ex = (elExchange && elExchange.value) || "";
if (ex) q.set("exchange_key", ex);
if (elFilterProfit && elFilterProfit.checked) q.set("filter_profit", "1");
@@ -304,6 +338,14 @@
return q.toString();
}
function fmtPnlStat(v) {
const n = Number(v);
if (!Number.isFinite(n)) return "—";
const cls = n >= 0 ? "pnl-pos" : "pnl-neg";
const text = (n >= 0 ? "+" : "") + n.toFixed(2) + "U";
return '<span class="' + cls + '">' + text + "</span>";
}
function renderExchangeOptions() {
if (!elExchange || !meta) return;
const cur = elExchange.value;
@@ -320,14 +362,47 @@
function renderStats() {
if (!elStats) return;
const st = dailyStats || { open_count: 0, by_exchange: {} };
const parts = ["今日开仓 " + (st.open_count || 0) + " 次"];
const label = periodLabel || "本日";
const sickPct = st.sick_pct != null ? st.sick_pct : 0;
let html =
'<div class="archive-stats-line"><strong>' +
esc(label) +
"</strong> · 开仓 " +
(st.open_count || 0) +
" 次 · 犯病 " +
(st.sick_count || 0) +
" 次(" +
sickPct +
"% · 盈亏 " +
fmtPnlStat(st.pnl_total) +
" · 剔除犯病盈亏 " +
fmtPnlStat(st.pnl_ex_sick) +
"</div>";
const byEx = st.by_exchange || {};
Object.keys(byEx)
.sort()
.forEach(function (ex) {
parts.push(exchangeLabel(ex) + " " + byEx[ex]);
const exKeys = Object.keys(byEx).sort();
if (exKeys.length) {
const exParts = exKeys.map(function (ex) {
const e = byEx[ex] || {};
const sickN = e.sick_count || 0;
const openN = e.open_count || 0;
const sickShare = openN ? Math.round((sickN / openN) * 1000) / 10 : 0;
return (
esc(exchangeLabel(ex)) +
" " +
openN +
"次/犯病" +
sickN +
"" +
sickShare +
"%/盈亏" +
fmtPnlStat(e.pnl_total) +
"/剔犯" +
fmtPnlStat(e.pnl_ex_sick)
);
});
elStats.textContent = parts.join(" · ");
html += '<div class="archive-stats-sub">' + exParts.join(" · ") + "</div>";
}
elStats.innerHTML = html;
}
function quotePreview(text) {
@@ -990,15 +1065,26 @@
setStatus(j.detail || "加载失败");
return;
}
periodMode = j.period || periodMode || "today";
periodLabel = j.period_label || periodLabel || "";
dateFrom = j.date_from || dateFrom || "";
dateTo = j.date_to || dateTo || "";
tradingDay = j.trading_day || tradingDay;
if (elTradingDay && tradingDay && !elTradingDay.value) elTradingDay.value = tradingDay;
if (elTradingDay && tradingDay) elTradingDay.value = tradingDay;
if (elDateFrom && dateFrom) elDateFrom.value = dateFrom;
if (elDateTo && dateTo) elDateTo.value = dateTo;
if (elQuoteDate && tradingDay && !elQuoteDate.value) elQuoteDate.value = tradingDay;
syncPeriodUI();
dailyTrades = j.trades || [];
dailyStats = j.stats || { open_count: 0, by_exchange: {} };
renderStats();
renderTrades();
setStatus(
(tradingDay || "当日") + " · " + dailyTrades.length + " 笔 · " + new Date().toLocaleTimeString()
(periodLabel || tradingDay || "当日") +
" · 列表 " +
dailyTrades.length +
" 笔 · " +
new Date().toLocaleTimeString()
);
}
@@ -1056,7 +1142,19 @@
if (elBtnRefresh) elBtnRefresh.addEventListener("click", loadDailyTrades);
if (elBtnSync) elBtnSync.addEventListener("click", syncAll);
if (elExchange) elExchange.addEventListener("change", loadDailyTrades);
if (elPeriodTabs) {
elPeriodTabs.addEventListener("click", function (ev) {
const btn = ev.target.closest(".archive-period-btn");
if (!btn) return;
const next = btn.getAttribute("data-period") || "today";
if (next === periodMode) return;
setPeriodMode(next);
loadDailyTrades();
});
}
if (elTradingDay) elTradingDay.addEventListener("change", loadDailyTrades);
if (elDateFrom) elDateFrom.addEventListener("change", loadDailyTrades);
if (elDateTo) elDateTo.addEventListener("change", loadDailyTrades);
[elFilterProfit, elFilterLoss, elFilterSick].forEach(function (el) {
if (el) el.addEventListener("change", loadDailyTrades);
});
@@ -1117,6 +1215,7 @@
if (!inited) {
loadMarkAutoPref();
setChartOpen(false);
syncPeriodUI();
bindEvents();
inited = true;
}