window.XiaobaiPageModules.register("pools", [
"limitPool",
"brokenView",
"downView",
"yesterdayView",
"performanceView",
], { bind: bindPoolEvents });
function getVisibleStocks() {
if (!state.dashboard) return [];
let rows = [...(state.dashboard.limits || [])];
if (state.filter === "1") rows = rows.filter((row) => number(row.streak) === 1);
if (state.filter === "2") rows = rows.filter((row) => number(row.streak) === 2);
if (state.filter === "3") rows = rows.filter((row) => number(row.streak) >= 3);
if (state.query) {
rows = rows.filter((row) => {
const haystack = `${row.code} ${row.name} ${row.sector} ${row.reason}`.toLowerCase();
return haystack.includes(state.query);
});
}
return rows.sort((left, right) => compareRows(left, right));
}
function renderLimitTable() {
if (!state.dashboard) return;
const rows = getVisibleStocks();
const allRows = state.dashboard.limits || [];
const body = document.querySelector("#limitTableBody");
body.innerHTML = rows.map((row, index) => `
| ${index + 1} |
${escapeHtml(row.name)}${escapeHtml(row.code)} |
${streakLabel(row.streak)} |
${signed(row.change)} |
${formatNumber(row.price, 2)} |
${escapeHtml(row.sector || "其他")} |
${escapeHtml(row.first_time || "")} |
${escapeHtml(row.last_time || "")} |
${limitOpenState(row)} |
${formatNumber(row.turnover_rate, 2)} |
${formatNumber(row.amount_billion, 2)} |
${formatLimitSealAmount(row.seal_amount_million)} |
${escapeHtml(row.reason || "")} |
`).join("");
bindStockRows(body);
setText("resultCount", `${rows.length} 只`);
setText("limitPoolSubtitle", `${allRows.length} 只 · 数据日期 ${displayCompactDate(state.dashboard.meta?.trade_date || elements.tradeDate.value)}`);
setText("limitAllCount", allRows.length);
setText("limitFirstCount", allRows.filter((row) => number(row.streak) === 1).length);
setText("limitSecondCount", allRows.filter((row) => number(row.streak) === 2).length);
setText("limitThreePlusCount", allRows.filter((row) => number(row.streak) >= 3).length);
document.querySelector("#emptyState").hidden = rows.length !== 0;
updateSortHeaders();
}
function limitOpenState(row) {
const openTimes = number(row.open_times);
const firstTime = String(row.first_time || "");
if (firstTime.startsWith("09:25") && openTimes === 0) return '一字';
if (openTimes >= 6) return `烂板×${openTimes}`;
return String(openTimes);
}
function formatLimitSealAmount(value) {
const amount = number(value);
if (!amount) return "";
return Math.round(amount).toLocaleString("zh-CN");
}
function renderBrokenTable(rows) {
const visibleRows = getVisibleBrokenRows(rows);
setText("brokenCount", `${rows.length} 只`);
setText("brokenMeta", ` · 触及涨停后未能封住 · 数据日期 ${displayCompactDate(state.dashboard?.meta?.trade_date || elements.tradeDate.value)}`);
const body = document.querySelector("#brokenTableBody");
body.innerHTML = visibleRows.map((row, index) => `
| ${index + 1} |
${escapeHtml(row.name)}${escapeHtml(row.code)} |
${signed(row.change)} |
${formatNumber(row.limitGap, 2)} |
${formatNumber(row.price, 2)} |
${escapeHtml(row.sector || "其他")} |
${escapeHtml(row.first_time || "")} |
${brokenOpenState(row)} |
${formatNumber(row.turnover_rate, 2)} |
${formatNumber(row.amount_billion, 2)} |
${escapeHtml(row.reason || "")} |
`).join("");
bindStockRows(body);
document.querySelector("#brokenEmptyState").hidden = visibleRows.length !== 0;
updateBrokenSortHeaders();
}
function getVisibleBrokenRows(rows = state.dashboard?.broken || []) {
let visibleRows = rows.map((row) => ({ ...row, limitGap: brokenLimitGap(row) }));
if (state.brokenQuery) {
visibleRows = visibleRows.filter((row) => `${row.code} ${row.name} ${row.sector}`.toLowerCase().includes(state.brokenQuery));
}
if (!state.brokenSortKey) return visibleRows;
return visibleRows.sort((left, right) => {
const result = number(left[state.brokenSortKey]) - number(right[state.brokenSortKey]);
return state.brokenSortDirection === "asc" ? result : -result;
});
}
function brokenLimitRate(row) {
const name = String(row.name || "").toUpperCase();
const code = String(row.code || "").replace(/\D/g, "");
if (name.includes("ST")) return 10;
if (/^(300|301|688|689)/.test(code)) return 20;
if (/^(4|8|92)/.test(code)) return 30;
return 10;
}
function brokenLimitGap(row) {
return Math.max(0, brokenLimitRate(row) - number(row.change));
}
function brokenOpenState(row) {
const openTimes = number(row.open_times);
return openTimes >= 6
? `反复炸 ×${openTimes}`
: String(openTimes);
}
function changeBrokenSort(key) {
if (state.brokenSortKey === key) state.brokenSortDirection = state.brokenSortDirection === "asc" ? "desc" : "asc";
else {
state.brokenSortKey = key;
state.brokenSortDirection = "desc";
}
renderBrokenTable(state.dashboard?.broken || []);
}
function updateBrokenSortHeaders() {
document.querySelectorAll("#brokenTable th[data-broken-sort]").forEach((header) => {
header.classList.remove("sort-asc", "sort-desc", "sorted");
header.setAttribute("aria-sort", "none");
if (header.dataset.brokenSort === state.brokenSortKey) {
header.classList.add(state.brokenSortDirection === "asc" ? "sort-asc" : "sort-desc", "sorted");
header.setAttribute("aria-sort", state.brokenSortDirection === "asc" ? "ascending" : "descending");
}
const arrow = header.querySelector(".arr");
if (arrow) arrow.textContent = header.classList.contains("sorted") ? (state.brokenSortDirection === "asc" ? "▲" : "▼") : "↕";
});
}
function renderDownTable(rows) {
const visibleRows = getVisibleDownRows(rows);
setText("downCount", `${rows.length} 只`);
setText("downMeta", ` · 观察退潮、高位风险与亏钱效应 · 数据日期 ${displayCompactDate(state.dashboard?.meta?.trade_date || elements.tradeDate.value)}`);
renderDownSectorCluster(rows);
const body = document.querySelector("#downTableBody");
body.innerHTML = visibleRows.map((row, index) => `
| ${index + 1} |
${escapeHtml(row.name)}${escapeHtml(row.code)} |
${signed(row.change)} |
${formatNumber(row.price, 2)} |
${escapeHtml(row.sector || "其他")} |
${formatNumber(row.turnover_rate, 2)} |
${formatNumber(row.amount_billion, 2)} |
${number(row.streak) > 0 ? number(row.streak) : ""} |
${escapeHtml(row.reason || "")} |
`).join("");
bindStockRows(body);
document.querySelector("#downEmptyState").hidden = visibleRows.length !== 0;
updateDownSortHeaders();
}
function getVisibleDownRows(rows = state.dashboard?.down_limits || []) {
let visibleRows = [...rows];
if (state.downQuery) {
visibleRows = visibleRows.filter((row) => `${row.code} ${row.name} ${row.sector}`.toLowerCase().includes(state.downQuery));
}
if (!state.downSortKey) return visibleRows;
return visibleRows.sort((left, right) => {
const result = number(left[state.downSortKey]) - number(right[state.downSortKey]);
return state.downSortDirection === "asc" ? result : -result;
});
}
function renderDownSectorCluster(rows) {
const counts = new Map();
rows.forEach((row) => {
const sector = String(row.sector || "其他").trim() || "其他";
if (sector === "其他") return;
counts.set(sector, (counts.get(sector) || 0) + 1);
});
const cluster = [...counts.entries()].sort((left, right) => right[1] - left[1])[0];
const element = document.querySelector("#downSectorCluster");
element.hidden = !cluster || cluster[1] < 2;
element.textContent = cluster && cluster[1] >= 2 ? `${cluster[0]}集中跌停 ×${cluster[1]}` : "";
}
function changeDownSort(key) {
if (state.downSortKey === key) state.downSortDirection = state.downSortDirection === "asc" ? "desc" : "asc";
else {
state.downSortKey = key;
state.downSortDirection = "asc";
}
renderDownTable(state.dashboard?.down_limits || []);
}
function updateDownSortHeaders() {
document.querySelectorAll("#downTable th[data-down-sort]").forEach((header) => {
header.classList.remove("sort-asc", "sort-desc", "sorted");
header.setAttribute("aria-sort", "none");
if (header.dataset.downSort === state.downSortKey) {
header.classList.add(state.downSortDirection === "asc" ? "sort-asc" : "sort-desc", "sorted");
header.setAttribute("aria-sort", state.downSortDirection === "asc" ? "ascending" : "descending");
}
const arrow = header.querySelector(".arr");
if (arrow) arrow.textContent = header.classList.contains("sorted") ? (state.downSortDirection === "asc" ? "▲" : "▼") : "↕";
});
}
function renderYesterdayTable(rows) {
const visibleRows = getVisibleYesterdayRows(rows);
const currentDate = displayCompactDate(state.dashboard?.meta?.trade_date || elements.tradeDate.value);
const previousDate = displayCompactDate(state.dashboard?.meta?.previous_trade_date || "");
setText("yesterdayCount", `${rows.length} 只`);
setText("yesterdayMeta", ` · 昨日 ${previousDate} → 今日 ${currentDate}`);
renderYesterdaySummary(rows);
const body = document.querySelector("#yesterdayTableBody");
body.innerHTML = visibleRows.map((row, index) => `
| ${index + 1} |
${escapeHtml(row.name)}${escapeHtml(row.code)} |
${number(row.prior_streak)} |
${signed(row.current_change)} |
${escapeHtml(row.outcome)} |
${number(row.current_streak) ? `${number(row.current_streak)}` : ""} |
${escapeHtml(row.sector || "其他")} |
${escapeHtml(row.reason || "")} |
`).join("");
bindStockRows(body);
document.querySelector("#yesterdayEmptyState").hidden = visibleRows.length !== 0;
updateYesterdayControls();
}
function getVisibleYesterdayRows(rows = state.dashboard?.yesterday_limits || []) {
let visibleRows = rows.filter((row) => {
if (state.yesterdayFilter === "advance") return row.outcome === "晋级";
if (state.yesterdayFilter === "positive") return number(row.current_change) > 0;
if (state.yesterdayFilter === "fail") return row.outcome === "断板";
if (state.yesterdayFilter === "risk") return ["炸板", "跌停"].includes(row.outcome);
return true;
});
if (state.yesterdayQuery) {
visibleRows = visibleRows.filter((row) => `${row.code} ${row.name} ${row.sector}`.toLowerCase().includes(state.yesterdayQuery));
}
if (!state.yesterdaySortKey) return visibleRows;
return visibleRows.sort((left, right) => {
const result = number(left[state.yesterdaySortKey]) - number(right[state.yesterdaySortKey]);
return state.yesterdaySortDirection === "asc" ? result : -result;
});
}
function renderYesterdaySummary(rows) {
const total = rows.length;
const advance = rows.filter((row) => row.outcome === "晋级").length;
const positive = rows.filter((row) => number(row.current_change) > 0).length;
const fail = rows.filter((row) => row.outcome === "断板").length;
const risk = rows.filter((row) => ["炸板", "跌停"].includes(row.outcome)).length;
const rate = (value) => total ? value / total * 100 : 0;
setText("yesterdayAllCount", total);
setText("yesterdayAdvanceCount", advance);
setText("yesterdayAdvanceRate", `晋级率 ${formatNumber(rate(advance), 1)}%`);
setText("yesterdayPositiveCount", positive);
setText("yesterdayPositiveRate", `兑现率 ${formatNumber(rate(positive), 1)}%`);
setText("yesterdayFailCount", fail);
setText("yesterdayFailRate", `占 ${formatNumber(rate(fail), 1)}%`);
setText("yesterdayRiskCount", risk);
setText("yesterdayRiskRate", `亏钱效应 ${formatNumber(rate(risk), 1)}%`);
}
function yesterdayOutcomeClass(outcome) {
return { "晋级": "advance", "断板": "fail", "炸板": "broken", "跌停": "down" }[outcome] || "fail";
}
function changeYesterdaySort(key) {
if (state.yesterdaySortKey === key) state.yesterdaySortDirection = state.yesterdaySortDirection === "asc" ? "desc" : "asc";
else {
state.yesterdaySortKey = key;
state.yesterdaySortDirection = "desc";
}
renderYesterdayTable(state.dashboard?.yesterday_limits || []);
}
function updateYesterdayControls() {
document.querySelectorAll("[data-yesterday-filter]").forEach((button) => {
const active = button.dataset.yesterdayFilter === state.yesterdayFilter;
button.classList.toggle("active", active);
button.setAttribute("aria-pressed", String(active));
});
document.querySelectorAll("#yesterdayTable th[data-yesterday-sort]").forEach((header) => {
header.classList.remove("sort-asc", "sort-desc", "sorted");
header.setAttribute("aria-sort", "none");
if (header.dataset.yesterdaySort === state.yesterdaySortKey) {
header.classList.add(state.yesterdaySortDirection === "asc" ? "sort-asc" : "sort-desc", "sorted");
header.setAttribute("aria-sort", state.yesterdaySortDirection === "asc" ? "ascending" : "descending");
}
const arrow = header.querySelector(".arr");
if (arrow) arrow.textContent = header.classList.contains("sorted") ? (state.yesterdaySortDirection === "asc" ? "▲" : "▼") : "↕";
});
}
function renderPerformance(rows) {
rows = normalizePerformanceRows(rows);
const currentDate = displayCompactDate(state.dashboard?.meta?.trade_date || elements.tradeDate.value);
const previousDate = displayCompactDate(state.dashboard?.meta?.previous_trade_date || "");
setText("performanceDateRange", `昨日 ${previousDate} → 今日 ${currentDate}`);
document.querySelector("#performanceCards").innerHTML = rows.map((row) => `
${escapeHtml(row.label)} → 今日${performanceRateState(row.advance_rate).label}
${formatNumber(row.advance_rate, 1)}%
晋级 ${number(row.advanced)} / 共 ${number(row.count)} 只
`).join("") || '暂无昨日涨停统计
';
renderPerformanceConclusion(rows);
renderMarketBreadth(state.dashboard?.overview || {});
}
function normalizePerformanceRows(rows) {
const groups = new Map();
(rows || []).forEach((row) => {
const level = Math.max(1, number(row.level));
const displayLevel = Math.min(level, 5);
const group = groups.get(displayLevel) || {
level: displayLevel,
label: displayLevel === 1 ? "昨日首板" : displayLevel === 5 ? "昨日5板+" : `昨日${displayLevel}板`,
count: 0,
advanced: 0,
positive: 0,
changeTotal: 0,
};
const count = number(row.count);
group.count += count;
group.advanced += number(row.advanced);
group.positive += count * number(row.positive_rate) / 100;
group.changeTotal += count * number(row.average_change);
groups.set(displayLevel, group);
});
return [...groups.values()]
.sort((left, right) => right.level - left.level)
.map((group) => ({
level: group.level,
label: group.label,
count: group.count,
advanced: group.advanced,
advance_rate: group.count ? group.advanced / group.count * 100 : 0,
positive_rate: group.count ? group.positive / group.count * 100 : 0,
average_change: group.count ? group.changeTotal / group.count : 0,
}));
}
function performanceRateState(rate) {
const value = number(rate);
if (value === 0) return { label: "失效", className: "is-neutral" };
if (value < 20) return { label: "危险", className: "is-warning" };
return { label: "活跃", className: "is-active" };
}
function renderPerformanceConclusion(rows) {
const container = document.querySelector("#performanceConclusion");
if (!rows.length) {
container.innerHTML = '暂无昨日梯队数据,暂不生成结论
';
return;
}
const sorted = [...rows].sort((left, right) => number(right.level) - number(left.level));
const highRows = sorted.filter((row) => number(row.level) >= 4);
const highAdvanced = highRows.reduce((total, row) => total + number(row.advanced), 0);
const highSamples = highRows.map((row) => escapeHtml(row.label)).join("、");
const strongest = [...rows].sort((left, right) => (
number(right.advance_rate) - number(left.advance_rate) || number(right.level) - number(left.level)
))[0];
const firstBoard = rows.find((row) => number(row.level) === 1);
const overview = state.dashboard?.overview || {};
const phase = overview.sentiment_phase || "观察";
const up = number(overview.up_count);
const down = number(overview.down_count);
const breadthRate = up + down > 0 ? up / (up + down) * 100 : 50;
const stance = breadthRate < 25 ? "宜守不宜攻" : breadthRate < 45 ? "控制仓位,聚焦核心" : "保持精选,跟随强势梯队";
const highText = highRows.length
? `高位晋级率${highAdvanced ? "仍有承接" : "全线失效"}:${highSamples}${highAdvanced ? `共晋级 ${highAdvanced} 只` : "今日均未晋级"};`
: "高位梯队暂无昨日样本,空间信号仍待确认;";
const strongestText = strongest
? `${escapeHtml(strongest.label)}晋级率最高,为 ${formatNumber(strongest.advance_rate, 1)}%(${number(strongest.advanced)} 只晋级 / 共 ${number(strongest.count)} 只);`
: "暂无相对占优梯队;";
const firstBoardText = firstBoard
? `首板基数 ${number(firstBoard.count)} 只,晋级率 ${formatNumber(firstBoard.advance_rate, 1)}%,低位接力${number(firstBoard.advance_rate) < 20 ? "胜率偏低" : "仍有活跃度"};`
: "首板梯队暂无有效样本;";
container.innerHTML = `
· ${highText}
· ${strongestText}
· ${firstBoardText}
· 结论:${stance},当前情绪周期「${escapeHtml(phase)}」。
`;
}
function changeSort(key) {
if (state.sortKey === key) state.sortDirection = state.sortDirection === "asc" ? "desc" : "asc";
else {
state.sortKey = key;
state.sortDirection = ["name", "code", "sector", "first_time", "last_time"].includes(key) ? "asc" : "desc";
}
renderLimitTable();
}
function compareRows(left, right) {
const leftValue = left[state.sortKey] ?? "";
const rightValue = right[state.sortKey] ?? "";
let result = typeof leftValue === "number" || typeof rightValue === "number"
? number(leftValue) - number(rightValue)
: String(leftValue).localeCompare(String(rightValue), "zh-CN", { numeric: true });
if (result === 0 && state.sortKey !== "first_time") result = String(left.first_time || "").localeCompare(String(right.first_time || ""));
return state.sortDirection === "asc" ? result : -result;
}
function updateSortHeaders() {
document.querySelectorAll("#limitTable th[data-sort]").forEach((header) => {
header.classList.remove("sort-asc", "sort-desc", "sorted");
const active = header.dataset.sort === state.sortKey;
if (active) header.classList.add(state.sortDirection === "asc" ? "sort-asc" : "sort-desc", "sorted");
const arrow = header.querySelector(".arr");
if (arrow) arrow.textContent = active ? (state.sortDirection === "asc" ? "▲" : "▼") : "↕";
});
}
function bindPoolEvents() {
document.querySelector("#stockSearch").addEventListener("input", (event) => {
state.query = event.target.value.trim().toLowerCase();
renderLimitTable();
});
document.querySelectorAll("[data-table-search]").forEach((input) => {
input.addEventListener("input", () => {
const query = input.value.trim().toLowerCase();
const body = document.querySelector(`#${CSS.escape(input.dataset.tableSearch)}`);
body?.querySelectorAll("tr").forEach((row) => {
row.hidden = Boolean(query) && !row.textContent.toLowerCase().includes(query);
});
});
});
document.querySelectorAll("[data-filter]").forEach((button) => {
button.addEventListener("click", () => {
document.querySelectorAll("[data-filter]").forEach((item) => item.classList.remove("active"));
button.classList.add("active");
state.filter = button.dataset.filter;
renderLimitTable();
});
});
document.querySelectorAll("#limitTable th[data-sort]").forEach((header) => {
header.addEventListener("click", () => changeSort(header.dataset.sort));
});
document.querySelector("#brokenSearch").addEventListener("input", (event) => {
state.brokenQuery = event.target.value.trim().toLowerCase();
renderBrokenTable(state.dashboard?.broken || []);
});
document.querySelectorAll("#brokenTable th[data-broken-sort]").forEach((header) => {
header.addEventListener("click", () => changeBrokenSort(header.dataset.brokenSort));
});
document.querySelector("#downSearch").addEventListener("input", (event) => {
state.downQuery = event.target.value.trim().toLowerCase();
renderDownTable(state.dashboard?.down_limits || []);
});
document.querySelectorAll("#downTable th[data-down-sort]").forEach((header) => {
header.addEventListener("click", () => changeDownSort(header.dataset.downSort));
});
document.querySelector("#yesterdaySearch").addEventListener("input", (event) => {
state.yesterdayQuery = event.target.value.trim().toLowerCase();
renderYesterdayTable(state.dashboard?.yesterday_limits || []);
});
document.querySelectorAll("[data-yesterday-filter]").forEach((button) => {
button.addEventListener("click", () => {
state.yesterdayFilter = button.dataset.yesterdayFilter;
renderYesterdayTable(state.dashboard?.yesterday_limits || []);
});
});
document.querySelectorAll("#yesterdayTable th[data-yesterday-sort]").forEach((header) => {
header.addEventListener("click", () => changeYesterdaySort(header.dataset.yesterdaySort));
});
document.querySelector("#exportButton").addEventListener("click", exportStocks);
document.querySelector("#brokenExportButton").addEventListener("click", exportBroken);
document.querySelector("#downExportButton").addEventListener("click", exportDown);
document.querySelector("#yesterdayExportButton").addEventListener("click", exportYesterday);
document.querySelector("#reasonForm").addEventListener("submit", saveReasonOverride);
}